Skip to content

Commit

Permalink
feat(inventory grops): group name modal (#1755)
Browse files Browse the repository at this point in the history
* feat(inventory grops): group name modal

* feat(inventory groups): updated test

* feat(inventory groups): updated the mock server config and api

* feat(inventory groups): added tests and fixed api requests

* feat(inventory groups): added useCallback and removed axios

* feat(inventory groups): more tests and chrome fix

* feat(inventory groups): updated package lock and babel

* feat(inventory-groups): updated the modal and api

* feat(inventory groups): updating debounce and tests

* feat(inventory groups): updated tests and schema validators

* feat(inventory groups): updated test for empty state

* feat(inventory groups): resolve test issues and updated configs

* feat(inventory grops): api request change
  • Loading branch information
Fewwy authored Feb 15, 2023
1 parent d360856 commit d9f1925
Show file tree
Hide file tree
Showing 14 changed files with 2,123 additions and 1,252 deletions.
19 changes: 18 additions & 1 deletion config/cypress.webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
/* global module, __dirname */
const { resolve } = require('path');
const webpack = require('webpack');
const config = require('@redhat-cloud-services/frontend-components-config');

const { config: webpackConfig, plugins } = config({
rootFolder: resolve(__dirname, '../')
rootFolder: resolve(__dirname, '../'),
customProxy: [
{
context: ['/api/inventory/v1/groups'], // you can adjust the `context` value to redirect only specific endpoints
target: 'http://localhost:4010', // default prism port
secure: false,
changeOrigin: true,
pathRewrite: { '^/api/inventory/v1': '' },
onProxyReq: (proxyReq) => {
proxyReq.setHeader('x-rh-identity', 'foobar'); // avoid 401 errors by providing neccessary security header
}
}
]
});

plugins.push(new webpack.DefinePlugin({
IS_DEV: true
}));

module.exports = {
...webpackConfig,
plugins
Expand Down
1 change: 1 addition & 0 deletions config/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ global.shallow = shallow;
global.render = render;
global.mount = mount;
global.React = React;
global.IS_DEV = true;
2,913 changes: 1,690 additions & 1,223 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"npm": ">=7.0.0"
},
"dependencies": {
"@data-driven-forms/common": "^3.19.3",
"@data-driven-forms/pf4-component-mapper": "^3.19.3",
"@data-driven-forms/react-form-renderer": "^3.19.3",
"@patternfly/react-core": "^4.265.2",
"@patternfly/react-icons": "^4.93.6",
"@patternfly/react-table": "^4.111.45",
Expand All @@ -15,6 +18,7 @@
"@redhat-cloud-services/frontend-components-utilities": "^3.3.9",
"@redhat-cloud-services/host-inventory-client": "1.0.116",
"@unleash/proxy-client-react": "^3.5.0",
"awesome-debounce-promise": "^2.1.0",
"classnames": "^2.3.1",
"graphql": "^15.6.0",
"html-webpack-plugin": "^5.5.0",
Expand Down Expand Up @@ -109,13 +113,13 @@
}
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/core": "^7.20.12",
"@babel/plugin-proposal-class-properties": "^7.16.0",
"@babel/plugin-proposal-decorators": "^7.16.0",
"@babel/plugin-proposal-object-rest-spread": "^7.16.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.16.0",
"@babel/preset-env": "^7.16.0",
"@babel/preset-env": "^7.18.6",
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.0",
"@cypress/code-coverage": "^3.10.0",
Expand Down Expand Up @@ -174,7 +178,7 @@
"test:ct": "BABEL_ENV=componentTest cypress run --component",
"test:openct": "BABEL_ENV=componentTest cypress open --component",
"coverage": "bash coverage.sh",
"startMockServer": "prism mock -d $npm_package_config_api_schema_path",
"startMockServer": "prism mock $npm_package_config_api_schema_path",
"start:mock": "MOCK=true run-p startMockServer start:proxy"
},
"insights": {
Expand Down
92 changes: 92 additions & 0 deletions src/components/InventoryGroups/Modals/CreateGroupModal.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable camelcase */
import React from 'react';
import { mount } from '@cypress/react';
import CreateGroupModal from './CreateGroupModal';
import {
TEXT_INPUT
} from '@redhat-cloud-services/frontend-components-utilities';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { getStore } from '../../../store';

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'
}
]
}
];

describe('Create Group Modal', () => {
before(() => {
cy.window().then(window => window.insights = {
chrome: {
isProd: false,
auth: {
getUser: () => {
return Promise.resolve({});
}
}
}
});
});

beforeEach(() => {

cy.intercept('POST', '**/api/inventory/v1/groups', {
statusCode: 504
}).as('create_group');
cy.intercept('GET', '**/api/inventory/v1/groups', {
statusCode: 200, body: {
...mockResponse
}
}).as('validate');

mount(
<MemoryRouter>
<Provider store={getStore()}>
<CreateGroupModal isModalOpen={true} reloadData={() => console.log('data reloaded')}/>
</Provider>
</MemoryRouter>
);
});

it('Input is fillable and firing a validation request that succeeds', () => {
cy.get(TEXT_INPUT).type('sre-group0');
cy.wait('@validate').then((xhr) => {
expect(xhr.request.url).to.contain('groups');}
);
cy.get(`button[type="submit"]`).should('have.attr', 'aria-disabled', 'true');
});
});
71 changes: 71 additions & 0 deletions src/components/InventoryGroups/Modals/CreateGroupModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { createGroupSchema } from './ModalSchemas/schemes';
import Modal from './Modal';
import apiWithToast from '../utils/apiWithToast';
import {
createGroup,
validateGroupName
} from '../utils/api';
import { useDispatch } from 'react-redux';
import awesomeDebouncePromise from 'awesome-debounce-promise';

const CreateGroupModal = ({
isModalOpen,
setIsModalOpen,
reloadData
}) => {
const dispatch = useDispatch();

const handleCreateGroup = useCallback(
(values) => {
const statusMessages = {
onSuccess: {
title: 'Success',
description: `${values.name} has been created successfully`
},
onError: { title: 'Error', description: 'Failed to create group' }
};
return apiWithToast(dispatch, () => createGroup(values), statusMessages);
},
[isModalOpen]
);

const schema = useMemo(() => {
const check = async (value) => {
const results = await validateGroupName(value);
if (results === true) {
throw 'Group name already exists';
}

return undefined;
};

// eslint-disable-next-line new-cap
const d = awesomeDebouncePromise(check, 500, { onlyResolvesLast: false });
return createGroupSchema(d);
}, []);

return (
<Modal
data-testid="create-group-modal"
isModalOpen={isModalOpen}
closeModal={() => setIsModalOpen(false)}
title="Create group"
submitLabel="Create"
schema={schema}
reloadData={reloadData}
onSubmit={handleCreateGroup}
/>
);
};

export default CreateGroupModal;

CreateGroupModal.propTypes = {
isModalOpen: PropTypes.bool,
setIsModalOpen: PropTypes.func,
reloadData: PropTypes.func,
deviceIds: PropTypes.array,
isOpen: PropTypes.bool
};
24 changes: 24 additions & 0 deletions src/components/InventoryGroups/Modals/CreateGroupModal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { getStore } from '../../../store';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';

import CreateGroupModal from './CreateGroupModal';

describe('CreateGroupModal', () => {
it('renders correctly', () => {
render(
<MemoryRouter>
<Provider store={getStore()}>
<CreateGroupModal isModalOpen={true} reloadData={() => console.log('data reloaded')}/>
</Provider>
</MemoryRouter>
);
expect(screen.getByRole('heading', { name: /Create group/ })).toBeInTheDocument();
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Create/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Cancel/ })).toBeInTheDocument();
});
});
72 changes: 72 additions & 0 deletions src/components/InventoryGroups/Modals/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { Modal } from '@patternfly/react-core';
import FormRenderer from '@data-driven-forms/react-form-renderer/form-renderer';
import FormTemplate from '@data-driven-forms/pf4-component-mapper/form-template';
import componentMapper from '@data-driven-forms/pf4-component-mapper/component-mapper';
import PropTypes from 'prop-types';

const RepoModal = ({
isModalOpen,
title,
titleIconVariant,
closeModal,
submitLabel,
schema,
initialValues,
variant,
reloadData,
size,
onSubmit
}) => {
return (
<Modal
ouiaId="group-modal"
variant={size ?? 'small'}
title={title}
titleIconVariant={titleIconVariant ?? null}
isOpen={isModalOpen}
onClose={closeModal}
>
<FormRenderer
schema={schema}
FormTemplate={(props) => (
<FormTemplate
{...props}
submitLabel={submitLabel}
disableSubmit={['invalid']}
buttonsProps={{
submit: { variant }
}}
/>
)}
initialValues={initialValues}
componentMapper={componentMapper}
//reload comes from the table and fetches fresh data
onSubmit={async (values) => {
await onSubmit(values);
setTimeout(async () => await reloadData(), 500);
closeModal();
}}
onCancel={() => closeModal()}
/>
</Modal>
);
};

RepoModal.propTypes = {
isModalOpen: PropTypes.bool,
title: PropTypes.string,
closeModal: PropTypes.func,
reloadData: PropTypes.func,
submitLabel: PropTypes.string,
schema: PropTypes.object,
initialValues: PropTypes.object,
variant: PropTypes.string,
onSubmit: PropTypes.func,
size: PropTypes.string,
additionalMappers: PropTypes.object,
titleIconVariant: PropTypes.any,
validatorMapper: PropTypes.object
};

export default RepoModal;
24 changes: 24 additions & 0 deletions src/components/InventoryGroups/Modals/ModalSchemas/schemes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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';

export const createGroupSchema = (namePresenceValidator) => ({
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'name',
label: 'Group name',
helperText:
'Can only contain letters, numbers, spaces, hyphens ( - ), and underscores( _ ).',
isRequired: true,
autoFocus: true,
validate: [
// async validator has to be first in the list
namePresenceValidator,
{ type: validatorTypes.REQUIRED },
{ type: validatorTypes.MAX_LENGTH, threshold: 50 },
nameValidator
]
}
]
});
Loading

0 comments on commit d9f1925

Please sign in to comment.