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-3729): added new actions to kebab and new modal #1794

Merged
merged 13 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
92 changes: 92 additions & 0 deletions src/components/InventoryGroups/Modals/AddHostToGroupModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Modal from './Modal';
import { addHostToGroup } from '../utils/api';
import apiWithToast from '../utils/apiWithToast';
import { useDispatch } from 'react-redux';
import { CreateGroupButton } from '../SmallComponents/CreateGroupButton';
import SearchInput from './SearchInput';
import { fetchGroups } from '../../../store/inventory-actions';
import { addHostSchema } from './ModalSchemas/schemes';
import CreateGroupModal from './CreateGroupModal';

const AddHostToGroupModal = ({
isModalOpen,
setIsModalOpen,
modalState,
reloadData
}) => {
const dispatch = useDispatch();
//we have to fetch groups to make them available in state
useEffect(() => {
dispatch(fetchGroups());

}, []);
const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false);

const handleAddDevices = (values) => {
const { group } = values;
const statusMessages = {
onSuccess: {
title: 'Success',
description: `System(s) have been added to ${group.toString()} successfully`
},
onError: { title: 'Error', description: `Failed to add ${modalState.name} to ${modalState.groupName}` }
};

apiWithToast(
dispatch,
() => addHostToGroup(parseInt(group.groupId), modalState.id),
gkarat marked this conversation as resolved.
Show resolved Hide resolved
statusMessages
);
};

return (
<>
<Modal
isModalOpen={isModalOpen}
closeModal={() => setIsModalOpen(false)}
title="Add to group"
submitLabel="Add"
schema={addHostSchema(modalState.name)}
additionalMappers={{
'search-input': {
component: SearchInput
},
'create-group-btn': {
component: CreateGroupButton,
closeModal: () => {
setIsCreateGroupModalOpen(true);
setIsModalOpen(false);
}
}
}}
initialValues={modalState}
onSubmit={handleAddDevices}
reloadData={reloadData}
/>
{isCreateGroupModalOpen && (
<CreateGroupModal
isModalOpen={isCreateGroupModalOpen}
setIsModalOpen={setIsCreateGroupModalOpen}
reloadData={() => console.log('data reloaded')}
/>
)}
</>
);
};

AddHostToGroupModal.propTypes = {
modalState: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
groupName: PropTypes.string
}),
isModalOpen: PropTypes.bool,
setIsModalOpen: PropTypes.func,
reloadData: PropTypes.func,
setIsCreateGroupModalOpen: PropTypes.func,
deviceIds: PropTypes.array
};

export default AddHostToGroupModal;
7 changes: 5 additions & 2 deletions src/components/InventoryGroups/Modals/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const RepoModal = ({
variant,
reloadData,
size,
onSubmit
onSubmit,
additionalMappers
}) => {
return (
<Modal
Expand All @@ -40,7 +41,9 @@ const RepoModal = ({
/>
)}
initialValues={initialValues}
componentMapper={componentMapper}
componentMapper={additionalMappers
? { ...additionalMappers, ...componentMapper }
: componentMapper}
//reload comes from the table and fetches fresh data
onSubmit={async (values) => {
await onSubmit(values);
Expand Down
31 changes: 31 additions & 0 deletions src/components/InventoryGroups/Modals/ModalSchemas/schemes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import validatorTypes from '@data-driven-forms/react-form-renderer/validator-types';
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
import { nameValidator } from '../../helpers/validate';
import { Text } from '@patternfly/react-core';

export const createGroupSchema = (namePresenceValidator) => ({
fields: [
Expand All @@ -22,3 +24,32 @@ export const createGroupSchema = (namePresenceValidator) => ({
}
]
});

const createDescription = (systemName) => {
return (
<Text>
Select a group to add <strong>{systemName}</strong> to, or create a new one.
</Text>
);
};

//this is a custom schema that is passed via additional mappers to the Modal component
//it allows to create custom item types in the modal

export const addHostSchema = (systemName) => ({
fields: [
{
component: componentTypes.PLAIN_TEXT,
name: 'description',
label: createDescription(systemName)
},
{
component: 'search-input',
name: 'group',
label: 'Select a group',
isRequired: true,
validate: [{ type: validatorTypes.REQUIRED }]
},
{ component: 'create-group-btn', name: 'create-group-btn' }
]
});
61 changes: 18 additions & 43 deletions src/components/InventoryGroups/Modals/RenameGroupModal.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,9 @@ import {
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { getStore } from '../../../store';
import groups from '../../../../cypress/fixtures/groups.json';

const mockResponse = [
{
count: 50,
page: 20,
per_page: 20,
total: 50,
results: [
{
created_at: '2020-02-09T10:16:07.996Z',
host_ids: ['bA6deCFc19564430AB814bf8F70E8cEf'],
id: '3f01b55457674041b75e41829bcee1dca',
name: 'sre-group0',
updated_at: '2020-02-09T10:16:07.996Z'
},
{
created_at: '2020-02-09T10:16:07.996Z',
host_ids: ['bA6deCFc19564430AB814bf8F70E8cEf'],
id: '3f01b55457674041b75e41829bcee1dca',
name: 'sre-group1',
updated_at: '2020-02-09T10:16:07.996Z'
},
{
created_at: '2020-02-09T10:16:07.996Z',
host_ids: ['bA6deCFc19564430AB814bf8F70E8cEf'],
id: '3f01b55457674041b75e41829bcee1dca',
name: 'sre-group2',
updated_at: '2020-02-09T10:16:07.996Z'
},
{
created_at: '2020-02-09T10:16:07.996Z',
host_ids: ['bA6deCFc19564430AB814bf8F70E8cEf'],
id: '3f01b55457674041b75e41829bcee1dca',
name: 'sre-group3',
updated_at: '2020-02-09T10:16:07.996Z'
}
]
}
];
const mockResponse = [groups];

describe('render Rename Group Modal', () => {
before(() => {
Expand Down Expand Up @@ -74,28 +38,39 @@ describe('render Rename Group Modal', () => {
}
}).as('rename');

});

it('Input is fillable and firing a validation request that succeeds', () => {
mount(
<MemoryRouter>
<Provider store={getStore()}>
<RenameGroupModal
isModalOpen={true}
reloadData={() => console.log('data reloaded')}
modalState={{ id: '1', name: 'sre-group' }}
modalState={{ id: '1', name: 'Ut occaeca' }}
/>
</Provider>
</MemoryRouter>
);
});

it('Input is fillable and firing a validation request that succeeds', () => {
cy.get(TEXT_INPUT).type('0');
cy.get(TEXT_INPUT).type('t');
cy.wait('@validate').then((xhr) => {
expect(xhr.request.url).to.contain('groups');}
);
cy.get(`button[type="submit"]`).should('have.attr', 'aria-disabled', 'true');
});

it('User can rename the group', () => {
mount(
<MemoryRouter>
<Provider store={getStore()}>
<RenameGroupModal
isModalOpen={true}
reloadData={() => console.log('data reloaded')}
modalState={{ id: '1', name: 'sre-group' }}
/>
</Provider>
</MemoryRouter>
);
cy.get(TEXT_INPUT).type('newname');
cy.get(`button[type="submit"]`).should('have.attr', 'aria-disabled', 'false');
cy.get(`button[type="submit"]`).click();
Expand Down
131 changes: 131 additions & 0 deletions src/components/InventoryGroups/Modals/SearchInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import {
HelperText,
HelperTextItem,
Select,
SelectOption
} from '@patternfly/react-core';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import debounce from 'lodash/debounce';

const SearchInput = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component makes some really multiple and ineffecient computations. I can see with profiler that the component and the list is re-rendered too many times (has constantly brown edges). Also the behavior is with the huge delays (it's well seen on the video)

Screencast.from.03-29-2023.11.23.18.AM.webm

const [storeGroups, setStoreGroups] = useState();
const [isLoading, setIsLoading] = useState(true);
//fetch data from the store
const fetchedGroupValues = useSelector(({ groups }) => groups?.data?.results);
//as soon data is fetched assign it to the storeGroups
useEffect(() => {
setStoreGroups(fetchedGroupValues);
}, [fetchedGroupValues]);
//select options is a constructed array of objects with values for dropdown
const [selectOptions, setSelectOptions] = useState([]);
//when storeGroups is changed - we create selectOptions
useEffect(() => {
setSelectOptions(
(storeGroups || []).reduce((acc, group) => {
acc.push({
DeviceGroup: {
ID: group.id,
Name: group.name,
UpdatedAt: group.updated_at,
CreatedAt: group.created_at
}
});
return acc;
}, [])
);
setIsLoading(false);
}, [storeGroups]);

const { change } = useFormApi();
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState(null);
const [searchTerm, setSearchTerm] = useState('');

const onToggle = (isOpen) => {
setIsOpen(isOpen);
};

const updateSelection = (value) => {
// Update state when an option has been selected.
setSelected(value);
setIsOpen(false);
change('group', value);
};

const onSelect = (_event, selection) => {
if (_event) {
updateSelection(selection);
}
};

const clearSelection = () => {
setSearchTerm('');
updateSelection(null);
};

const onFilter = (_event, value) => {
// Only filter the groups data if there is a search term
if (value) {
const filteredGroups = fetchedGroupValues.filter(group =>
group.name.toLowerCase().includes(value.toLowerCase())
);
// Update the state with the filtered groups data
setStoreGroups(filteredGroups);
} else {
// If the search term is empty, use the original groups data from the store
setStoreGroups(fetchedGroupValues);
}

// Update the search term state
setSearchTerm(value);
};

return (
<>
<HelperText>
{!isLoading && !selected && isOpen && selectOptions.length ? (
<HelperTextItem variant="warning" className="pf-u-font-weight-bold">
Over {selectOptions.length} results found. Refine your search.
</HelperTextItem>
) : (
<HelperTextItem className="pf-u-font-weight-bold">
Select a group
</HelperTextItem>
)}
</HelperText>
<Select
variant="typeahead"
typeAheadAriaLabel="Select a group"
onToggle={onToggle}
onSelect={onSelect}
onClear={clearSelection}
selections={selected ? selected : searchTerm}
isOpen={isOpen}
onFilter={debounce(onFilter, 300)}
aria-labelledby="typeahead-select-id-1"
placeholderText="Type or click to select a group"
isInputValuePersisted={true}
maxHeight={'180px'}
>
{selectOptions.length === 0
? []
: selectOptions?.map(({ DeviceGroup }) => (
<SelectOption
key={DeviceGroup.ID}
value={{
toString: () => DeviceGroup.Name,
groupId: DeviceGroup.ID
}}
{...(DeviceGroup.description && {
description: DeviceGroup.description
})}
/>
))}
</Select>
</>
);
};

export default SearchInput;
Loading