Skip to content

Refactor: Migrate colum bulk opeation component to untitled ui#26463

Merged
anuj-kumary merged 12 commits intomainfrom
migrate-column-bulk
Mar 17, 2026
Merged

Refactor: Migrate colum bulk opeation component to untitled ui#26463
anuj-kumary merged 12 commits intomainfrom
migrate-column-bulk

Conversation

@anuj-kumary
Copy link
Copy Markdown
Member

Describe your changes:

Migrated the Column Bulk Operation component to Untitled UI and removed all usages of Material UI components from this module.

Screen.Recording.2026-03-13.at.1.12.17.PM.mov

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 65%
66.02% (57451/87010) 45.72% (30405/66496) 48.6% (9109/18742)

karanh37
karanh37 previously approved these changes Mar 16, 2026
@gitar-bot
Copy link
Copy Markdown

gitar-bot bot commented Mar 16, 2026

Code Review ⚠️ Changes requested 5 resolved / 9 findings

Migrates column bulk operation component to Untitled UI with several issues unresolved: fragile button selector for expand, services search using keyword wildcard instead of full-text query, and getAllDescendantIds lacking cycle guard causing potential infinite loops and O(n²) rescanning. Five findings were addressed including keyboard event propagation and dead props removal.

⚠️ Edge Case: Services search uses keyword wildcard instead of full-text query

📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx:112 📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx:117

The search parameter was removed from getServiceDetails calls and is no longer passed to searchService. This means the Elasticsearch q parameter defaults to * (wildcard match-all), and the search term is applied exclusively via queryFilter as keyword wildcard clauses (*term* on name.keyword and displayName.keyword).

This is a behavioral regression: previously, the search term was passed as the q parameter for analyzed full-text matching (which handles tokenization, partial words, etc.). Now only exact substring matches on keyword fields will work. For example, searching for 'mysql' won't match a service named 'My MySQL Production Service' stored in a non-keyword field.

Additionally, keyword fields are case-sensitive by default in Elasticsearch, so searching 'mysql' won't find 'MySQL' unless the field has a normalizer configured.

⚠️ Bug: getAllDescendantIds has no cycle guard — can infinite loop

📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:1445

The BFS in getAllDescendantIds (line 1445) has no visited set. If structParentId references ever form a cycle (e.g., due to a data bug), the queue will grow unboundedly, hanging the browser tab. This is a defensive programming issue — even if cycles shouldn't exist in the data model, a single corrupt row would freeze the UI.

Suggested fix
Add a `visited` Set to break cycles:
```tsx
const getAllDescendantIds = useCallback(
  (parentId: string): string[] => {
    const allRows = columnGridListing.allRows;
    const result: string[] = [parentId];
    const visited = new Set<string>([parentId]);
    const queue = [parentId];
    while (queue.length > 0) {
      const current = queue.shift() as string;
      const children = allRows.filter((r) => r.structParentId === current);
      for (const c of children) {
        if (!visited.has(c.id)) {
          visited.add(c.id);
          result.push(c.id);
          queue.push(c.id);
        }
      }
    }
    return result;
  },
  [columnGridListing.allRows]
);
```
💡 Quality: Fragile getByRole('button').first() selector for expand

📄 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts:975 📄 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts:997

The expand-button locator was changed from button.expand-button (a specific class) to structRow.getByRole('button').first(). This silently picks whatever button happens to be first in the row. If the row layout changes (e.g., a selection or action button is added before the expand toggle), the test will click the wrong element and fail with a confusing error. Consider adding a data-testid to the expand button in the component code and using getByTestId here instead.

💡 Performance: getAllDescendantIds rescans allRows per BFS node — O(n²)

📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:1452 📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:1638

getAllDescendantIds calls allRows.filter((r) => r.structParentId === current) for every node in the BFS queue. For a table with N columns, each BFS step is O(N), making the total O(N²) per call. This function is then called inside forEach loops over selected entities (lines 1638, 1664), compounding the cost. For tables with hundreds of columns this could cause noticeable UI lag during selection changes.

Suggested fix
Build a parentId→children lookup map once:
```tsx
const childrenMap = useMemo(() => {
  const map = new Map<string, string[]>();
  for (const r of columnGridListing.allRows) {
    if (r.structParentId) {
      const arr = map.get(r.structParentId) ?? [];
      arr.push(r.id);
      map.set(r.structParentId, arr);
    }
  }
  return map;
}, [columnGridListing.allRows]);
```
Then use `childrenMap.get(current) ?? []` inside the BFS instead of `.filter()`.
✅ 5 resolved
Bug: onClick on column-name Buttons may not stop propagation for keyboard

📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:932 📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:1067
Two column-name <Button> components use the native onClick prop (lines 932 and 1067) instead of react-aria's onPress. While mouse clicks are intercepted by react-aria's internal usePress and won't bubble to the parent Table.Row, keyboard activations (Enter/Space) fire a synthetic click event that may propagate differently, potentially toggling row selection unintentionally when the user keyboard-activates the column name link.

The expand buttons in the same file correctly use onPress. These two call sites should be consistent.

Quality: Dead props passed to ColumnGridTableRow should be removed

📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/components/ColumnGridTableRow.tsx:54 📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/components/ColumnGridTableRow.tsx:59 📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/components/ColumnGridTableRow.tsx:60
isIndeterminate, onSelect, and onGroupSelect are still declared in the ColumnGridTableRowProps interface and passed from the parent ColumnGrid.component.tsx, but they are immediately destructured into underscore-prefixed unused variables (_isIndeterminate, _onSelect, _onGroupSelect). Now that the Table component handles selection natively, these props are dead code and should be cleaned up from both the interface and the call site to avoid confusion.

Quality: ROW_BG_TRANSITION string used as className instead of style

📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/components/ColumnGridTableRow.tsx:43 📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/components/ColumnGridTableRow.tsx:83
In ColumnGridTableRow.tsx, the constant ROW_BG_TRANSITION = 'background-color 0.4s ease-in-out' (a CSS transition value) is passed into classNames() as if it were a CSS class name (e.g., line 83). This produces an invalid class name string like tw:bg-utility-warning-50 background-color 0.4s ease-in-out which has no effect. The transition styling appears to be covered by the tw:transition-colors utility on the row class, making this usage a no-op at best. Either remove the ROW_BG_TRANSITION usages from classNames or convert it to a Tailwind class.

Quality: "Discard changes" test no longer enters data before discarding

📄 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts:725 📄 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts:760
The test should discard changes when closing drawer without saving previously had a step that opened the drawer and filled in a display name ('Temporary Display Name') before closing it. That step was removed in this commit, so the test now:

  1. Selects a column
  2. Presses Escape (no drawer is open at this point — only a checkbox was clicked)
  3. Reopens the drawer and asserts displayNameValue is not 'Temporary Display Name'

This assertion will always pass trivially because no data was ever entered, making the test a no-op for its stated purpose. The test title says "discard changes" but it never creates changes to discard.

Bug: Nesting indentation removed for struct child rows

📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:962 📄 openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:329
The old code indented struct child rows based on their nesting level:

const nestingPadding = (entity.nestingLevel || 1) * 24;
style={{ paddingLeft: `${nestingPadding}px` }}

This was removed in the refactor and not replaced with any alternative. nestingLevel is still computed and stored on each row (lines 329, 341) but never used for visual rendering. This means deeply nested STRUCT columns (e.g., address.street.number) will all render flush-left with no visual hierarchy, making it impossible for users to understand parent-child relationships.

🤖 Prompt for agents
Code Review: Migrates column bulk operation component to Untitled UI with several issues unresolved: fragile button selector for expand, services search using keyword wildcard instead of full-text query, and `getAllDescendantIds` lacking cycle guard causing potential infinite loops and O(n²) rescanning. Five findings were addressed including keyboard event propagation and dead props removal.

1. 💡 Quality: Fragile `getByRole('button').first()` selector for expand
   Files: openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts:975, openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts:997

   The expand-button locator was changed from `button.expand-button` (a specific class) to `structRow.getByRole('button').first()`. This silently picks whatever button happens to be first in the row. If the row layout changes (e.g., a selection or action button is added before the expand toggle), the test will click the wrong element and fail with a confusing error. Consider adding a `data-testid` to the expand button in the component code and using `getByTestId` here instead.

2. ⚠️ Edge Case: Services search uses keyword wildcard instead of full-text query
   Files: openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx:112, openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx:117

   The `search` parameter was removed from `getServiceDetails` calls and is no longer passed to `searchService`. This means the Elasticsearch `q` parameter defaults to `*` (wildcard match-all), and the search term is applied exclusively via `queryFilter` as keyword wildcard clauses (`*term*` on `name.keyword` and `displayName.keyword`).
   
   This is a behavioral regression: previously, the search term was passed as the `q` parameter for analyzed full-text matching (which handles tokenization, partial words, etc.). Now only exact substring matches on keyword fields will work. For example, searching for 'mysql' won't match a service named 'My MySQL Production Service' stored in a non-keyword field.
   
   Additionally, `keyword` fields are case-sensitive by default in Elasticsearch, so searching 'mysql' won't find 'MySQL' unless the field has a normalizer configured.

3. ⚠️ Bug: `getAllDescendantIds` has no cycle guard — can infinite loop
   Files: openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:1445

   The BFS in `getAllDescendantIds` (line 1445) has no `visited` set. If `structParentId` references ever form a cycle (e.g., due to a data bug), the `queue` will grow unboundedly, hanging the browser tab. This is a defensive programming issue — even if cycles shouldn't exist in the data model, a single corrupt row would freeze the UI.

   Suggested fix:
   Add a `visited` Set to break cycles:
   ```tsx
   const getAllDescendantIds = useCallback(
     (parentId: string): string[] => {
       const allRows = columnGridListing.allRows;
       const result: string[] = [parentId];
       const visited = new Set<string>([parentId]);
       const queue = [parentId];
       while (queue.length > 0) {
         const current = queue.shift() as string;
         const children = allRows.filter((r) => r.structParentId === current);
         for (const c of children) {
           if (!visited.has(c.id)) {
             visited.add(c.id);
             result.push(c.id);
             queue.push(c.id);
           }
         }
       }
       return result;
     },
     [columnGridListing.allRows]
   );
   ```

4. 💡 Performance: `getAllDescendantIds` rescans allRows per BFS node — O(n²)
   Files: openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:1452, openmetadata-ui/src/main/resources/ui/src/pages/ColumnBulkOperations/ColumnGrid/ColumnGrid.component.tsx:1638

   `getAllDescendantIds` calls `allRows.filter((r) => r.structParentId === current)` for every node in the BFS queue. For a table with N columns, each BFS step is O(N), making the total O(N²) per call. This function is then called inside `forEach` loops over selected entities (lines 1638, 1664), compounding the cost. For tables with hundreds of columns this could cause noticeable UI lag during selection changes.

   Suggested fix:
   Build a parentId→children lookup map once:
   ```tsx
   const childrenMap = useMemo(() => {
     const map = new Map<string, string[]>();
     for (const r of columnGridListing.allRows) {
       if (r.structParentId) {
         const arr = map.get(r.structParentId) ?? [];
         arr.push(r.id);
         map.set(r.structParentId, arr);
       }
     }
     return map;
   }, [columnGridListing.allRows]);
   ```
   Then use `childrenMap.get(current) ?? []` inside the BFS instead of `.filter()`.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud
Copy link
Copy Markdown

@anuj-kumary anuj-kumary merged commit 956237c into main Mar 17, 2026
26 checks passed
@anuj-kumary anuj-kumary deleted the migrate-column-bulk branch March 17, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants