Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ESSNTL-3728): Enable multiple hosts addition to group #1798

Merged
merged 16 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cypress/fixtures/hosts.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@
"reporter": "adipisicing veniam velit",
"created": "1962-06-25T23:00:00.0Z",
"account": null,
"group_name": "abc",
"mac_addresses": null,
"provider_id": "aute ut sit",
"facts": [
Expand Down
29 changes: 28 additions & 1 deletion cypress/support/interceptors.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import groupsSecondPage from '../fixtures/groupsSecondPage.json';
import groupDetailFixtures from '../fixtures/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C.json';
import hostsFixtures from '../fixtures/hosts.json';

export { hostsFixtures, groupDetailFixtures };
export const groupsInterceptors = {
'successful with some items': () =>
cy
Expand Down Expand Up @@ -63,6 +64,23 @@ export const groupDetailInterceptors = {
}
)
.as('getGroupDetail'),
'successful with hosts': () =>
cy
.intercept(
'GET',
'/api/inventory/v1/groups/620f9ae75A8F6b83d78F3B55Af1c4b2C',
{
statusCode: 200,
body: {
...groupDetailFixtures,
results: [{
...groupDetailFixtures.results[0],
host_ids: ['host-1', 'host-2']
}]
}
}
)
.as('getGroupDetail'),
empty: () =>
cy
.intercept(
Expand Down Expand Up @@ -162,7 +180,16 @@ export const featureFlagsInterceptors = {
cy.intercept('GET', '/feature_flags*', {
statusCode: 200,
body: {
toggles: []
toggles: [
{
name: 'hbi.ui.inventory-groups',
enabled: true,
variant: {
name: 'disabled',
enabled: true
}
}
]
}
}).as('getFeatureFlag');
}
Expand Down
19 changes: 17 additions & 2 deletions src/components/GroupSystems/GroupSystems.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CHIP_GROUP,
DROPDOWN_TOGGLE,
hasChip,
MODAL,
PAGINATION_VALUES,
SORTING_ORDERS,
TEXT_INPUT,
Expand Down Expand Up @@ -38,7 +39,7 @@ import _ from 'lodash';

const GROUP_NAME = 'foobar';
const ROOT = 'div[id="group-systems-table"]';
const TABLE_HEADERS = ['Name', 'Tags', 'OS', 'Update methods', 'Last seen'];
const TABLE_HEADERS = ['Name', 'Tags', 'OS', 'Update method', 'Last seen'];
const SORTABLE_HEADERS = ['Name', 'OS', 'Last seen'];
const DEFAULT_ROW_COUNT = 50;

Expand Down Expand Up @@ -300,7 +301,21 @@ describe('selection and bulk selection', () => {
});

describe('actions', () => {
// TBA
beforeEach(() => {
cy.intercept('*', { statusCode: 200 });
hostsInterceptors.successful();

mountTable();

cy.wait('@getHosts');
});

it('can open systems add modal', () => {
cy.get('button').contains('Add systems').click();
cy.get(MODAL).find('h1').contains('Add systems');

cy.wait('@getHosts');
});
});

describe('edge cases', () => {
Expand Down
144 changes: 94 additions & 50 deletions src/components/GroupSystems/GroupSystems.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
import { Button } from '@patternfly/react-core';
import { fitContent, TableVariant } from '@patternfly/react-table';
import difference from 'lodash/difference';
import map from 'lodash/map';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectEntity } from '../../store/inventory-actions';
import { clearFilters, selectEntity } from '../../store/inventory-actions';
import AddSystemsToGroupModal from '../InventoryGroups/Modals/AddSystemsToGroupModal';
import InventoryTable from '../InventoryTable/InventoryTable';

export const bulkSelectConfig = (dispatch, selectedNumber, noneSelected, pageSelected, rowsNumber) => ({
count: selectedNumber,
id: 'bulk-select-groups',
items: [
{
title: 'Select none (0)',
onClick: () => dispatch(selectEntity(-1, false)),
props: { isDisabled: noneSelected }
},
{
title: `${pageSelected ? 'Deselect' : 'Select'} page (${
rowsNumber
} items)`,
onClick: () => dispatch(selectEntity(0, !pageSelected))
}
// TODO: Implement "select all"
],
onSelect: (value) => {
dispatch(selectEntity(0, value));
},
checked: selectedNumber > 0 // TODO: support partial selection (dash sign) in FEC BulkSelect
});

const prepareColumns = (initialColumns) => {
// hides the "groups" column
const columns = initialColumns.filter(({ key }) => key !== 'groups');

// additionally insert the "update methods" column
// additionally insert the "update method" column
columns.splice(columns.length - 1 /* must be penultimate */, 0, {
key: 'update_method',
title: 'Update methods',
title: 'Update method',
sortKey: 'update_method',
transforms: [fitContent],
renderFunc: (value, hostId, systemData) =>
Expand All @@ -29,7 +54,7 @@ const prepareColumns = (initialColumns) => {
return columns;
};

const GroupSystems = ({ groupName }) => {
const GroupSystems = ({ groupName, groupId }) => {
const dispatch = useDispatch();

const selected = useSelector(
Expand All @@ -42,58 +67,77 @@ const GroupSystems = ({ groupName }) => {
const pageSelected =
difference(displayedIds, [...selected.keys()]).length === 0;

const [isModalOpen, setIsModalOpen] = useState(false);

const resetTable = () => {
dispatch(clearFilters());
dispatch(selectEntity(-1, false));
};

useEffect(() => {
return () => {
resetTable();
};
}, []);

return (
<div id='group-systems-table'>
<InventoryTable
columns={prepareColumns}
getEntities={async (items, config, showTags, defaultGetEntities) =>
await defaultGetEntities(
items,
// filter systems by the group name
{
...config,
filters: {
...config.filters,
groupName: [groupName] // TODO: the param is not yet supported by `apiHostGetHostList`
}
},
showTags
)
}
tableProps={{
isStickyHeader: true,
variant: TableVariant.compact,
canSelectAll: false
}}
bulkSelect={{
count: selected.size,
id: 'bulk-select-groups',
items: [
{
title: 'Select none (0)',
onClick: () => dispatch(selectEntity(-1, false)),
props: { isDisabled: noneSelected }
},
{
title: `${pageSelected ? 'Deselect' : 'Select'} page (${
rows.length
} items)`,
onClick: () => dispatch(selectEntity(0, !pageSelected))
}
// TODO: Implement "select all"
],
onSelect: (value) => {
dispatch(selectEntity(0, value));
},
checked: selected.size > 0 // TODO: support partial selection (dash sign) in FEC BulkSelect
}}
/>
{
isModalOpen && <AddSystemsToGroupModal
isModalOpen={isModalOpen}
setIsModalOpen={(value) => {
resetTable();
setIsModalOpen(value);
}
}
groupId={groupId}
groupName={groupName}
/>
}
{
!isModalOpen &&
<InventoryTable
columns={prepareColumns}
getEntities={async (items, config, showTags, defaultGetEntities) =>
await defaultGetEntities(
items,
// filter systems by the group name
{
...config,
filters: {
...config.filters,
groupName: [groupName] // TODO: the param is not yet supported by `apiHostGetHostList`
}
},
showTags
)
}
tableProps={{
isStickyHeader: true,
variant: TableVariant.compact,
canSelectAll: false
}}
bulkSelect={bulkSelectConfig(dispatch, selected.size, noneSelected, pageSelected, rows.length)}
>
<Button
variant='primary'
onClick={() => {
resetTable();
setIsModalOpen(true);
}}

>
Add systems
</Button>
</InventoryTable>
}
</div>
);
};

GroupSystems.propTypes = {
groupName: PropTypes.string.isRequired
groupName: PropTypes.string.isRequired,
groupId: PropTypes.string.isRequired
};

export default GroupSystems;
11 changes: 6 additions & 5 deletions src/components/GroupSystems/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { EmptyState, EmptyStateBody, Spinner } from '@patternfly/react-core';
import PropTypes from 'prop-types';
import React from 'react';
import { useSelector } from 'react-redux';
import NoGroupsEmptyState from '../InventoryGroups/NoGroupsEmptyState';
import NoSystemsEmptyState from '../InventoryGroupDetail/NoSystemsEmptyState';
import GroupSystems from './GroupSystems';

const GroupSystemsWrapper = ({ groupName }) => {
const GroupSystemsWrapper = ({ groupName, groupId }) => {
const { uninitialized, loading, data } = useSelector((state) => state.groupDetail);
const hosts = data?.results?.[0]?.host_ids /* can be null */ || [];

Expand All @@ -16,13 +16,14 @@ const GroupSystemsWrapper = ({ groupName }) => {
</EmptyStateBody>
</EmptyState>
) : hosts.length > 0 ? (
<GroupSystems groupName={groupName}/>
<GroupSystems groupId={groupId} groupName={groupName} />
) :
<NoGroupsEmptyState />;
<NoSystemsEmptyState groupId={groupId} groupName={groupName} />;
};

GroupSystemsWrapper.propTypes = {
groupName: PropTypes.string.isRequired
groupName: PropTypes.string.isRequired,
groupId: PropTypes.string.isRequired
};

export default GroupSystemsWrapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const InventoryGroupDetail = ({ groupId }) => {
aria-label="Group systems tab"
>
<PageSection>
<GroupSystems groupName={groupName}/>
<GroupSystems groupName={groupName} groupId={groupId}/>
</PageSection>
</Tab>
<Tab
Expand Down
23 changes: 19 additions & 4 deletions src/components/InventoryGroupDetail/NoSystemsEmptyState.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
Button,
EmptyState,
Expand All @@ -10,33 +10,48 @@ import {
import { ExternalLinkAltIcon, PlusCircleIcon } from '@patternfly/react-icons';

import { global_palette_black_600 as globalPaletteBlack600 } from '@patternfly/react-tokens/dist/js/global_palette_black_600';
import AddSystemsToGroupModal from '../InventoryGroups/Modals/AddSystemsToGroupModal';
import PropTypes from 'prop-types';

const NoSystemsEmptyState = ({ groupId, groupName }) => {
const [isModalOpen, setIsModalOpen] = useState(false);

const NoSystemsEmptyState = () => {
return (
<EmptyState
data-ouia-component-id="empty-state"
data-ouia-component-type="PF4/EmptyState"
data-ouia-safe={true}
>
<AddSystemsToGroupModal
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
groupId={groupId}
groupName={groupName}
/>
<EmptyStateIcon icon={PlusCircleIcon} color={globalPaletteBlack600.value} />
<Title headingLevel="h4" size="lg">
No systems added
</Title>
<EmptyStateBody>
To manage systems more effectively, add systems to the group.
</EmptyStateBody>
<Button variant="primary" onClick={() => {}}>Add systems</Button>
<Button variant="primary" onClick={() => setIsModalOpen(true)}>
Add systems
</Button>
<EmptyStateSecondaryActions>
<Button
variant="link"
icon={<ExternalLinkAltIcon />}
iconPosition="right"
// TODO: component={(props) => <a href='' {...props} />}
>
Learn more about system groups
</Button>
</EmptyStateSecondaryActions>
</EmptyState>
);};

NoSystemsEmptyState.propTypes = {
groupId: PropTypes.string,
groupName: PropTypes.string
};
export default NoSystemsEmptyState;
Loading