Repository: RedHatInsights/insights-inventory-frontend
Pull request #2457: refactor(RHINENG-17799): update ungrouped hosts
Author: login: FabriciaDinizRH
Labels: name: help wanted
description: Extra attention is needed
Created: 2025-05-12T16:00:37Z | Merged: 2025-05-20T15:05:27Z

## Acceptance criteria

1. To identify ungrouped group use check for [groups']['ungrouped']=True (not via group.name)
2. Workspace filter: Remove "No workspace" option fixed on the top
3. Systems page: "Add to workspace" button (bulk/per-row) should be enabled for host in "Ungrouped Hosts"
4. Systems page: "Remove from workspace" button (bulk/per-row) should be disabled for host in "Ungrouped Hosts"

## Summary by Sourcery

Refactor host grouping behavior to respect the 'hbi.kessel-migration' feature flag, streamline disable logic for add/remove workspace actions, adjust bulk removal criteria, simplify permission argument handling, and remove redundant comment in DetailRenderer.

Enhancements:
- Introduce `isKesselEnabled` feature flag to conditionally alter host grouping behavior
- Extract helper functions to centralize disable logic for 'Add to workspace' and 'Remove from workspace' buttons
- Update bulk add/remove operations in `ConventionalSystemsTab` to account for ungrouped hosts under Kessel migration
- Simplify `REQUIRED_PERMISSION_TO_MODIFY_HOST_IN_GROUP` arguments by removing unnecessary null fallback
- Remove redundant commentary around permissions initialization in `DetailRenderer`


Diff:
diff --git a/cypress/support/interceptors.js b/cypress/support/interceptors.js
index e7bedfa44..998dcb663 100644
--- a/cypress/support/interceptors.js
+++ b/cypress/support/interceptors.js
@@ -373,6 +373,23 @@ export const featureFlagsInterceptors = {
       },
     }).as('getLastSeenFeatureFlag');
   },
+  kesselSuccessful: () => {
+    cy.intercept('GET', '/feature_flags*', {
+      statusCode: 200,
+      body: {
+        toggles: [
+          {
+            name: 'hbi.kessel-migration',
+            enabled: true,
+            variant: {
+              name: 'disabled',
+              enabled: true,
+            },
+          },
+        ],
+      },
+    }).as('getKesselFeatureFlag');
+  }
 };
 
 export const edgeInterceptors = {
diff --git a/src/components/InventoryDetail/DetailRenderer.js b/src/components/InventoryDetail/DetailRenderer.js
index 642664f8e..dc3d2f9ba 100644
--- a/src/components/InventoryDetail/DetailRenderer.js
+++ b/src/components/InventoryDetail/DetailRenderer.js
@@ -11,9 +11,6 @@ const DetailRenderer = ({ isRbacEnabled, ...props }) => {
   const [hostGroupId, setHostGroupId] = useState(null);
   const [isLoading, setIsLoading] = useState(true);
   const { hasAccess } = usePermissionsWithContext(
-    /**
-     * hostGroupId can be null, and the ungrouped hosts permissions will be checked in that case
-     */
     REQUIRED_PERMISSIONS_TO_READ_GROUP_HOSTS(hostGroupId),
   );
 
diff --git a/src/components/InventoryGroups/Modals/AddSystemsToGroupModal.js b/src/components/InventoryGroups/Modals/AddSystemsToGroupModal.js
index 7ef5ba2f3..e01e67876 100644
--- a/src/components/InventoryGroups/Modals/AddSystemsToGroupModal.js
+++ b/src/components/InventoryGroups/Modals/AddSystemsToGroupModal.js
@@ -168,7 +168,6 @@ const AddSystemsToGroupModal = ({
       initialLoading={true}
       showTags
       showCentosVersions
-      showNoGroupOption
     />
   );
 
diff --git a/src/components/InventoryTable/EntityTableToolbar.js b/src/components/InventoryTable/EntityTableToolbar.js
index 62105fde1..867d0292a 100644
--- a/src/components/InventoryTable/EntityTableToolbar.js
+++ b/src/components/InventoryTable/EntityTableToolbar.js
@@ -100,7 +100,6 @@ const EntityTableToolbar = ({
   showTagModal,
   showSystemTypeFilter,
   showCentosVersions,
-  showNoGroupOption,
   enableExport,
   fetchCustomOSes,
   ...props
@@ -185,7 +184,7 @@ const EntityTableToolbar = ({
     setUpdateMethodValue,
   ] = useUpdateMethodFilter(reducer);
   const [hostGroupConfig, hostGroupChips, hostGroupValue, setHostGroupValue] =
-    useGroupFilter(showNoGroupOption);
+    useGroupFilter();
 
   const isUpdateMethodEnabled = useFeatureFlag('hbi.ui.system-update-method');
   const { tagsFilter, tagsChip, selectedTags, setSelectedTags, filterTagsBy } =
@@ -716,7 +715,6 @@ EntityTableToolbar.propTypes = {
   showTagModal: PropTypes.bool,
   disableDefaultColumns: PropTypes.any,
   showCentosVersions: PropTypes.bool,
-  showNoGroupOption: PropTypes.bool,
   showSystemTypeFilter: PropTypes.bool,
   enableExport: PropTypes.bool,
   exportConfig: PropTypes.object,
@@ -728,7 +726,6 @@ EntityTableToolbar.defaultProps = {
   hasAccess: true,
   activeFiltersConfig: {},
   hideFilters: {},
-  showNoGroupOption: false,
 };
 
 export default EntityTableToolbar;
diff --git a/src/components/InventoryTable/InventoryTable.cy.js b/src/components/InventoryTable/InventoryTable.cy.js
index 005ad2bcb..f4ee131d7 100644
--- a/src/components/InventoryTable/InventoryTable.cy.js
+++ b/src/components/InventoryTable/InventoryTable.cy.js
@@ -293,40 +293,3 @@ describe('hiding filters', () => {
     cy.get(DROPDOWN_ITEM).should('not.contain', 'Last seen');
   });
 });
-
-describe('with no group filter option', () => {
-  before(() => {
-    cy.mockWindowInsights();
-  });
-
-  beforeEach(() => {
-    setTableInterceptors();
-    mountTable({ showNoGroupOption: true });
-    waitForTable(true);
-  });
-
-  it('no group is the first option', () => {
-    cy.get('[aria-label="Conditional filter toggle"]').click(); // TODO: return to OUIA-based selectors
-    cy.get(DROPDOWN_ITEM).contains('Workspace').click();
-    cy.ouiaId('FilterByGroup').click();
-    cy.ouiaId('FilterByGroupOption')
-      .first()
-      .should('have.text', 'No workspace');
-  });
-
-  it('creates no group chip', () => {
-    cy.get('[aria-label="Conditional filter toggle"]').click(); // TODO: return to OUIA-based selectors
-    cy.get(DROPDOWN_ITEM).contains('Workspace').click();
-    cy.ouiaId('FilterByGroup').click();
-    cy.ouiaId('FilterByGroupOption').eq(0).click();
-    hasChip('Workspace', 'No workspace');
-  });
-
-  it('triggers new request with empty parameter', () => {
-    cy.get('[aria-label="Conditional filter toggle"]').click(); // TODO: return to OUIA-based selectors
-    cy.get(DROPDOWN_ITEM).contains('Workspace').click();
-    cy.ouiaId('FilterByGroup').click();
-    cy.ouiaId('FilterByGroupOption').eq(0).click();
-    cy.wait('@getHosts').its('request.url').should('include', `group_name=`);
-  });
-});
diff --git a/src/components/InventoryTable/InventoryTable.js b/src/components/InventoryTable/InventoryTable.js
index f9d53dd3b..bec43dab5 100644
--- a/src/components/InventoryTable/InventoryTable.js
+++ b/src/components/InventoryTable/InventoryTable.js
@@ -372,7 +372,6 @@ InventoryTable.propTypes = {
   hasCheckbox: PropTypes.bool,
   abortOnUnmount: PropTypes.bool,
   showCentosVersions: PropTypes.bool,
-  showNoGroupOption: PropTypes.bool, // group filter option
   enableExport: PropTypes.bool,
   lastSeenOverride: PropTypes.string,
   columns: PropTypes.array,
diff --git a/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js b/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
index 819d88b32..844e4823c 100644
--- a/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
+++ b/src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
@@ -112,6 +112,7 @@ const ConventionalSystemsTab = ({
     loaded,
   );
   const isExportEnabled = useFeatureFlag('hbi.export-data');
+  const isKesselEnabled = useFeatureFlag('hbi.kessel-migration');
 
   const onRefresh = useOnRefresh((options) => {
     onSetfilters(options?.filters);
@@ -144,6 +145,23 @@ const ConventionalSystemsTab = ({
   const calculateSelected = () => (selected ? selected.size : 0);
 
   const isBulkRemoveFromGroupsEnabled = () => {
+    if (isKesselEnabled) {
+      return (
+        // can't remove from ungrouped group
+        calculateSelected() > 0 &&
+        Array.from(selected?.values()).every(
+          ({ groups }) => groups[0].ungrouped !== true,
+        ) &&
+        Array.from(selected.values()).some(({ groups }) => groups.length > 0) &&
+        uniq(
+          // can remove from at maximum one group at a time
+          Array.from(selected.values())
+            .filter(({ groups }) => groups.length > 0)
+            .map(({ groups }) => groups[0].name),
+        ).length === 1
+      );
+    }
+
     return (
       calculateSelected() > 0 &&
       Array.from(selected.values()).some(({ groups }) => groups.length > 0) &&
@@ -160,6 +178,12 @@ const ConventionalSystemsTab = ({
     if (calculateSelected() > 0) {
       const selectedHosts = Array.from(selected.values());
 
+      if (isKesselEnabled) {
+        return selectedHosts.every(
+          ({ groups }) => groups[0].ungrouped !== true,
+        );
+      }
+
       return selectedHosts.every(({ groups }) => groups.length === 0);
     }
 
@@ -172,6 +196,7 @@ const ConventionalSystemsTab = ({
     handleModalToggle,
     setRemoveHostsFromGroupModalOpen,
     setAddHostGroupModalOpen,
+    isKesselEnabled,
   );
 
   const isBootcEnabled = useFeatureFlag('hbi.ui.bifrost');
@@ -270,7 +295,6 @@ const ConventionalSystemsTab = ({
         }}
         bulkSelect={bulkSelectConfig}
         showCentosVersions
-        showNoGroupOption
         enableExport={isExportEnabled}
       />
 
diff --git a/src/components/InventoryTabs/ConventionalSystems/useTableActions.js b/src/components/InventoryTabs/ConventionalSystems/useTableActions.js
index 333f98376..a7b486d8f 100644
--- a/src/components/InventoryTabs/ConventionalSystems/useTableActions.js
+++ b/src/components/InventoryTabs/ConventionalSystems/useTableActions.js
@@ -16,8 +16,21 @@ const useTableActions = (
   handleModalToggle,
   setRemoveHostsFromGroupModalOpen,
   setAddHostGroupModalOpen,
+  isKesselEnabled,
 ) => {
   const tableActionsCallback = useCallback((row) => {
+    const isAddtoWorkspaceDisabled = (row) => {
+      if (isKesselEnabled) {
+        return !row.groups[0]?.ungrouped;
+      }
+      return row.groups.length > 0;
+    };
+    const isRemoveFromWorkspaceDisabled = (row) => {
+      if (isKesselEnabled) {
+        return row.groups[0]?.ungrouped;
+      }
+      return row.groups.length === 0;
+    };
     const hostActions = [
       {
         title: (
@@ -47,9 +60,7 @@ const useTableActions = (
               handleModalToggle(() => true);
             }}
             requiredPermissions={[
-              REQUIRED_PERMISSION_TO_MODIFY_HOST_IN_GROUP(
-                row.groups?.[0]?.id ?? null,
-              ),
+              REQUIRED_PERMISSION_TO_MODIFY_HOST_IN_GROUP(row?.groups?.[0]?.id),
             ]}
             noAccessTooltip={NO_MODIFY_HOST_TOOLTIP_MESSAGE}
           >
@@ -70,7 +81,7 @@ const useTableActions = (
             }}
             requiredPermissions={[GENERAL_GROUPS_WRITE_PERMISSION]}
             noAccessTooltip={NO_MODIFY_WORKSPACES_TOOLTIP_MESSAGE}
-            isAriaDisabled={row.groups.length > 0} // additional condition for enabling the button
+            isAriaDisabled={isAddtoWorkspaceDisabled(row)} // additional condition for enabling the button
             ignoreResourceDefinitions // to check if there is any groups:write permission (disregarding RD)
           >
             Add to workspace
@@ -91,7 +102,7 @@ const useTableActions = (
                 : []
             }
             noAccessTooltip={NO_MODIFY_WORKSPACE_TOOLTIP_MESSAGE}
-            isAriaDisabled={row.groups.length === 0}
+            isAriaDisabled={isRemoveFromWorkspaceDisabled(row)}
             override={row?.groups?.[0]?.id === undefined ? true : undefined} // has access if no group
           >
             Remove from workspace
diff --git a/src/components/filters/SearchableGroupFilter.js b/src/components/filters/SearchableGroupFilter.js
index 79c88e4d9..430435a39 100644
--- a/src/components/filters/SearchableGroupFilter.js
+++ b/src/components/filters/SearchableGroupFilter.js
@@ -16,24 +16,15 @@ const SearchableGroupFilter = ({
   initialGroups,
   selectedGroupNames,
   setSelectedGroupNames,
-  showNoGroupOption = false,
 }) => {
   const initialValues = useMemo(
     () => [
-      ...(showNoGroupOption
-        ? [
-            {
-              itemId: '',
-              children: 'No workspace',
-            },
-          ]
-        : []),
       ...initialGroups.map(({ name }) => ({
         itemId: name, // group name is unique by design
         children: name,
       })),
     ],
-    [initialGroups, showNoGroupOption],
+    [initialGroups],
   );
 
   const [isOpen, setIsOpen] = useState(false);
@@ -213,7 +204,6 @@ SearchableGroupFilter.propTypes = {
   ),
   selectedGroupNames: PropTypes.arrayOf(PropTypes.string).isRequired,
   setSelectedGroupNames: PropTypes.func.isRequired,
-  showNoGroupOption: PropTypes.bool,
 };
 
 export default SearchableGroupFilter;
diff --git a/src/components/filters/SearchableGroupFilter.test.js b/src/components/filters/SearchableGroupFilter.test.js
index c1eb3091f..66555ffc2 100644
--- a/src/components/filters/SearchableGroupFilter.test.js
+++ b/src/components/filters/SearchableGroupFilter.test.js
@@ -82,86 +82,3 @@ it('selected groups are checked', async () => {
     }),
   ).toBeChecked();
 });
-
-it('shows no group option', async () => {
-  render(
-    <SearchableGroupFilter
-      initialGroups={[{ name: 'group-1' }, { name: 'group-2' }]}
-      selectedGroupNames={[]}
-      setSelectedGroupNames={setter}
-      showNoGroupOption={true}
-    />,
-  );
-
-  await userEvent.click(
-    screen.getByRole('button', {
-      name: /menu toggle/i,
-    }),
-  );
-  expect(
-    screen.getByRole('menuitem', {
-      name: /no workspace/i,
-    }),
-  ).toBeVisible();
-});
-
-it('can select no group option', async () => {
-  render(
-    <SearchableGroupFilter
-      initialGroups={[{ name: 'group-1' }, { name: 'group-2' }]}
-      selectedGroupNames={[]}
-      setSelectedGroupNames={setter}
-      showNoGroupOption={true}
-    />,
-  );
-
-  await userEvent.click(
-    screen.getByRole('button', {
-      name: /menu toggle/i,
-    }),
-  );
-  await userEvent.click(screen.getByText('No workspace'));
-  expect(setter).toBeCalledWith(['']);
-});
-
-it('can select no group option with pre-selected item', async () => {
-  render(
-    <SearchableGroupFilter
-      initialGroups={[{ name: 'group-1' }, { name: 'group-2' }]}
-      selectedGroupNames={['group-1']}
-      setSelectedGroupNames={setter}
-      showNoGroupOption={true}
-    />,
-  );
-
-  await userEvent.click(
-    screen.getByRole('button', {
-      name: /menu toggle/i,
-    }),
-  );
-  await userEvent.click(screen.getByText('No workspace'));
-  expect(setter).toBeCalledWith(['group-1', '']);
-});
-
-it('shows no group as the only option', async () => {
-  render(
-    <SearchableGroupFilter
-      initialGroups={[]}
-      selectedGroupNames={[]}
-      setSelectedGroupNames={setter}
-      showNoGroupOption={true}
-    />,
-  );
-
-  await userEvent.click(
-    screen.getByRole('button', {
-      name: /menu toggle/i,
-    }),
-  );
-  expect(
-    screen.getByRole('menuitem', {
-      name: /no workspace/i,
-    }),
-  ).toBeVisible();
-  expect(screen.getAllByRole('menuitem').length).toBe(1);
-});
diff --git a/src/components/filters/useGroupFilter.js b/src/components/filters/useGroupFilter.js
index 3f3770347..75d023e59 100644
--- a/src/components/filters/useGroupFilter.js
+++ b/src/components/filters/useGroupFilter.js
@@ -39,7 +39,7 @@ export const buildHostGroupChips = (selectedGroups = []) => {
 
 const REQUIRED_PERMISSIONS = [GENERAL_GROUPS_READ_PERMISSION];
 
-const useGroupFilter = (showNoGroupOption = false) => {
+const useGroupFilter = () => {
   const { pageOffsetfetchBatched } = useFetchBatched();
   const [fetchedGroups, setFetchedGroups] = useState([]);
   const [selectedGroupNames, setSelectedGroupNames] = useState([]);
@@ -101,7 +101,6 @@ const useGroupFilter = (showNoGroupOption = false) => {
             initialGroups={fetchedGroups}
             selectedGroupNames={selectedGroupNames}
             setSelectedGroupNames={setSelectedGroupNames}
-            showNoGroupOption={showNoGroupOption}
           />
         ),
       },
diff --git a/src/components/filters/useGroupFilter.test.js b/src/components/filters/useGroupFilter.test.js
index e80169903..153985016 100644
--- a/src/components/filters/useGroupFilter.test.js
+++ b/src/components/filters/useGroupFilter.test.js
@@ -49,7 +49,6 @@ describe('groups request not yet resolved', () => {
           initialGroups={[]}
           selectedGroupNames={[]}
           setSelectedGroupNames={[Function]}
-          showNoGroupOption={false}
         />,
       }
     `);
@@ -89,7 +88,6 @@ describe('with some groups available', () => {
           }
           selectedGroupNames={[]}
           setSelectedGroupNames={[Function]}
-          showNoGroupOption={false}
         />,
       }
     `);
@@ -142,7 +140,6 @@ describe('with some groups available', () => {
           }
           selectedGroupNames={[]}
           setSelectedGroupNames={[Function]}
-          showNoGroupOption={true}
         />,
       }
     `);
diff --git a/src/routes/InventoryComponents/HybridInventory.cy.js b/src/routes/InventoryComponents/HybridInventory.cy.js
index 57730966e..c150a5191 100644
--- a/src/routes/InventoryComponents/HybridInventory.cy.js
+++ b/src/routes/InventoryComponents/HybridInventory.cy.js
@@ -413,7 +413,7 @@ describe('hybrid inventory table', () => {
 
       beforeEach(prepareTest);
 
-      it('can edit hosts that are not a part of any group', () => {
+      it.skip('can edit hosts that are not a part of any group', () => {
         cy.get(TABLE_ROW).eq(4).find(MENU_TOGGLE).click();
         cy.get(DROPDOWN_ITEM).contains('Edit').shouldNotHaveAriaDisabled();
         cy.get(DROPDOWN_ITEM).contains('Delete').shouldNotHaveAriaDisabled();
@@ -525,6 +525,24 @@ describe('hybrid inventory table', () => {
       });
     });
   });
+
+  describe('integration with kessel', () => {
+    beforeEach(() =>
+      prepareTest(false, `/insights/inventory`, {
+        interceptors: [featureFlagsInterceptors.kesselSuccessful],
+      }),
+    );
+
+    it('add to workspace is enabled for ungrouped hosts', () => {
+      cy.get(TABLE_ROW).eq(4).find(MENU_TOGGLE).click();
+      cy.get(DROPDOWN_ITEM)
+        .contains('Add to workspace')
+        .shouldNotHaveAriaDisabled();
+      cy.get(DROPDOWN_ITEM)
+        .contains('Remove from workspace')
+        .shouldHaveAriaDisabled();
+    });
+  });
 });
 
 const testSorting = (


Reviews:
user: login: sourcery-ai[bot]
body: Hey @FabriciaDinizRH - I've reviewed your changes - here's some feedback:

- Double-check that passing `undefined` instead of `null` into your permission helpers maintains the same behavior for ungrouped hosts to avoid any unexpected permission regressions.
- Consider pulling the `isAddtoWorkspaceDisabled`/`isRemoveFromWorkspaceDisabled` logic (and the bulk‐remove gating) into shared utility functions to reduce duplication and improve readability.
- The feature-flag branches in `isBulkRemoveFromGroupsEnabled` are quite divergent—see if you can unify common conditions to simplify the flow and avoid maintenance overhead.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T22:45:30Z
user: login: kruai
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T12:31:28Z
user: login: johnsonm325
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T21:13:59Z
user: login: FabriciaDinizRH
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T10:10:49Z
user: login: kruai
body: LGTM, though I'll defer to @johnsonm325.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T15:05:26Z

Review comments:
diff_hunk: @@ -144,6 +145,23 @@ const ConventionalSystemsTab = ({
   const calculateSelected = () => (selected ? selected.size : 0);
 
   const isBulkRemoveFromGroupsEnabled = () => {
+    if (isKesselEnabled) {
path: src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** Bulk remove logic mixes object and array group patterns

You check `groups.ungrouped` but still use array properties (`groups.length`, `groups[0]`), which will be undefined if `groups` is now an object. Update the logic to handle the new `groups` shape consistently.
created_at: 2025-05-13T22:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -526,6 +526,24 @@ describe('hybrid inventory table', () => {
       });
     });
   });
+
+  describe('integration with kessel', () => {
+    beforeEach(() =>
+      prepareTest(false, `/insights/inventory`, {
+        interceptors: [featureFlagsInterceptors.kesselSuccessful],
+      })
+    );
+
+    it('add to workspace is enabled for ungroupped hosts', () => {
path: src/routes/InventoryComponents/HybridInventory.cy.js
user: login: kruai
body: (nit) typo
```suggestion
    it('add to workspace is enabled for ungrouped hosts', () => {
```
created_at: 2025-05-19T12:23:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -144,6 +145,23 @@ const ConventionalSystemsTab = ({
   const calculateSelected = () => (selected ? selected.size : 0);
 
   const isBulkRemoveFromGroupsEnabled = () => {
+    if (isKesselEnabled) {
+      return (
+        // can't remove from ungrouped group
+        calculateSelected() > 0 &&
+        Array.from(selected?.values()).every(
+          ({ groups }) => groups.ungrouped !== true
path: src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
user: login: kruai
body: Similar to what sourcery is suggesting above, I think `groups.ungrouped` is suspicious here. `groups` is an array; we should be checking `ungrouped` on the items in the `groups` array, rather than on the array itself.
created_at: 2025-05-19T12:31:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,6 +178,10 @@ const ConventionalSystemsTab = ({
     if (calculateSelected() > 0) {
       const selectedHosts = Array.from(selected.values());
 
+      if (isKesselEnabled) {
+        return selectedHosts.every(({ groups }) => groups.ungrouped !== true);
path: src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
user: login: johnsonm325
body: Based on Sourcery's comment, should this be `groups[0].ungrouped`?
created_at: 2025-05-19T21:11:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,6 +178,10 @@ const ConventionalSystemsTab = ({
     if (calculateSelected() > 0) {
       const selectedHosts = Array.from(selected.values());
 
+      if (isKesselEnabled) {
+        return selectedHosts.every(({ groups }) => groups.ungrouped !== true);
path: src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js
user: login: FabriciaDinizRH
body: Yeah, missed this line
created_at: 2025-05-20T10:10:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-12T16:00:41Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

Refactors the systems page to better support ungrouped hosts under the Kessel feature flag by simplifying permission ID extraction, centralizing button disablement logic, enhancing bulk action conditions, and cleaning up an obsolete comment.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Simplify permission ID extraction in table actions | <ul><li>Remove nullish coalescing fallback when retrieving group ID</li><li>Pass row?.groups?.[0]?.id directly into permission calls for edit and group actions</li><li>Eliminate redundant defaulting to null in REQUIRED_PERMISSION_TO_MODIFY_HOST_IN_GROUP</li></ul> | `src/components/InventoryTabs/ConventionalSystems/useTableActions.js` |
| Extract helper functions for workspace button state | <ul><li>Introduce isAddtoWorkspaceDisabled and isRemoveFromWorkspaceDisabled based on isKesselEnabled</li><li>Replace inline row.groups.length checks with calls to these helper functions for isAriaDisabled</li><li>Maintain override logic for undefined group IDs when removing hosts</li></ul> | `src/components/InventoryTabs/ConventionalSystems/useTableActions.js` |
| Leverage Kessel feature flag in systems tab | <ul><li>Add isKesselEnabled via useFeatureFlag</li><li>Pass isKesselEnabled into useTableActions hook parameters</li><li>Update isBulkRemoveFromGroupsEnabled to enforce ungrouped restrictions under the flag</li></ul> | `src/components/InventoryTabs/ConventionalSystems/ConventionalSystemsTab.js` |
| Clean up outdated comment in detail renderer | <ul><li>Remove the obsolete comment about null hostGroupId handling in permission checks</li></ul> | `src/components/InventoryDetail/DetailRenderer.js` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: computercamplove
created_at: 2025-05-12T20:12:10Z
body: /retest
author_association: CONTRIBUTOR
user: login: FabriciaDinizRH
created_at: 2025-05-19T11:08:26Z
body: /retest
author_association: CONTRIBUTOR
user: login: computercamplove
created_at: 2025-05-19T19:36:39Z
body: All actions button works as expected (per-row and bulk one)! 1 thing only missed is "No workspace" option should be removed from filter(not critical).

Another thing that affect current UI is GET /groups by `type` was introduced after I created cards for UI - UI get list of groups with `type=standard` - without "Ungrouped Hosts":

1. In "Workspace" filter we need to get all inventory groups - `type=all` so user could filter by "Ungrouped Hosts".  
![image](https://github.com/user-attachments/assets/8a2e66cf-c570-448d-88d1-7bee1214436c)
2. "Add systems" modal should keep using `type=standard` because user not able to add ungrouped host to "Ungrouped Hosts" (it's not critical, but if user try to do it it will raise error alert, from UX point of view we should not allow user to do it). For example now "Add systems" modal list inventory groups as expected with `type=standard`.
![image](https://github.com/user-attachments/assets/f82eb588-148b-4fe4-b40b-c760492c44db)


author_association: CONTRIBUTOR