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 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
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(group.groupId, modalState.id),
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
113 changes: 113 additions & 0 deletions src/components/InventoryGroups/Modals/SearchInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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';

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 { change } = useFormApi();
const [isLoading, setIsLoading] = useState(true);
//fetch data from the store
const storeGroups = useSelector(({ groups }) => groups?.data?.results);

//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 [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);
//this is requried to make select component pass the saved data up to the modal
change('group', value);
};

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

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

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}
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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable camelcase */

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import SearchInput from '../SearchInput';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import groups from '../../../../../cypress/fixtures/groups.json';

describe('SearchInput', () => {
let mockStore;
const initialStore = {
groups
};
beforeEach(() => {
mockStore = configureStore();
});

test('displays select options when the user clicks on the component', async () => {
const store = mockStore(initialStore);
render(<Provider store={store}><SearchInput /></Provider>);
fireEvent.click(screen.getByRole('textbox', { placeholder: 'Type or click to select a group' }));
const options = await screen.findAllByRole('option');
expect(options).toHaveLength(1);
});
});
Loading