diff --git a/app/javascript/components/settings-users-form/helper.js b/app/javascript/components/settings-users-form/helper.js new file mode 100644 index 00000000000..f130b114f33 --- /dev/null +++ b/app/javascript/components/settings-users-form/helper.js @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import { Button } from 'carbon-components-react'; + +/** button component used as a mapper to change the password */ +export const ChangePasswordButton = (props) => { + const { newRecord, enableConfirmPassword, isConfirmPasswordEnabled } = props; + const [isChangingPassword, setIsChangingPassword] = useState(false); + + const handleClick = () => { + /**Enable change password button if its not a new record */ + if (!newRecord) { + enableConfirmPassword(!isConfirmPasswordEnabled); + setIsChangingPassword(!isChangingPassword); + } + }; + + return !newRecord ? ( +
+ +
+ ) : null; +}; diff --git a/app/javascript/components/settings-users-form/index.jsx b/app/javascript/components/settings-users-form/index.jsx new file mode 100644 index 00000000000..877b76a0d89 --- /dev/null +++ b/app/javascript/components/settings-users-form/index.jsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect } from 'react'; +import MiqFormRenderer from '@@ddf'; +import PropTypes from 'prop-types'; +import createSchema from './schema'; +import { resources } from '../../spec/schedule-form/data'; +import handleFailure from '../../helpers/handle-failure'; +import componentMapper from '../../forms/mappers/componentMapper'; +import { ChangePasswordButton } from './helper'; + +const SettingsUsersForm = ({ recordId }) => { + const newRecord = recordId === 'new'; + + const [data, setData] = useState({ + isLoading: !!recordId, + groups: [], + initialValues: undefined, + userid: '', + }); + + + const enableConfirmPassword = (enable) => { + setIsConfirmPasswordEnabled(enable); + }; + + /** State variable to control the Confirm Password field */ + const [isConfirmPasswordEnabled, setIsConfirmPasswordEnabled] = useState(!newRecord); + + const mapper = { + ...componentMapper, + 'changePassword': ChangePasswordButton, + }; + + const validatorMapper = { + 'same-password': () => (value, allValues) => value !== allValues.password ? 'Password do not match' : undefined + } + + const redirectUrl = (newRecord, button) => `/ops/settings_users_helper/${newRecord}?button=${button}`; + + const USER_ACTIONS = { + CREATE: 'create', + SAVE: 'save', + CANCEL: 'cancel', + RESET: 'reset', + }; + + /** Generate dropdown options from resources */ + const groupOptions = (resources) => resources.map((item) => ({label: item.description, value: item.id})) + + /** Fetch data from the API on component mount */ + useEffect(() => { + if (recordId && !newRecord) { + Promise.all([ + API.get(`/api/groups?expand=resources`), + API.get(`/api/users/${recordId}?expand=resources/`)]) + .then(([groups, users]) => { + console.log(groups,"groupid"); + console.log(users,"userid"); + setData({ + ...data, + isLoading: false, + groups: groupOptions(groups.resources), + initialValues: users, + userid: users.userid || '', + }); + }); + } else { + API.get(`/api/groups?expand=resources`).then(({resources}) => { + setData({ + ...data, + groups: groupOptions(resources), + isLoading: false + }); + }) + } + }, [recordId]); + + console.log(data,"initial values") + + /** Function to handle form submission */ + const onSubmit = (values, newRecord, recordId) => { + const userPayload = { + userid: values.userid, + password: values.password, + name: values.name, + email:values.email, + // group: { id:20}, + group: values.current_group_id, + }; + + console.log(values,"values") + const url = newRecord ? '/api/users' : `/api/users/${recordId}`; + API.post(url, userPayload, { + skipErrors: [400, 500] + }) + .then((response) => { + const createdRecordId = newRecord ? response.id : recordId; + window.miqJqueryRequest(redirectUrl(createdRecordId)); + }) + .catch(handleFailure); + + }; + + const onCancel = () => { + /** If it's a new record, redirect to the previous page */ + if (newRecord) { + window.miqJqueryRequest(redirectUrl(recordId, USER_ACTIONS.CANCEL)); + } else { + /** For an existing user, reset the form values to the initial state */ + setData({ initialValues, isLoading: false }); + } + }; + + return !data.isLoading && ( + }} + onSubmit={onSubmit} + onCancel={() => onCancel()} + canReset={!newRecord} + buttonsLabels={{ + submitLabel: newRecord ? __('Add') : __('Save') + }} + validatorMapper={validatorMapper} + /> + ); +}; + +SettingsUsersForm.propTypes = { + recordId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, +}; + +export default SettingsUsersForm; diff --git a/app/javascript/components/settings-users-form/schema.js b/app/javascript/components/settings-users-form/schema.js new file mode 100644 index 00000000000..be09294d019 --- /dev/null +++ b/app/javascript/components/settings-users-form/schema.js @@ -0,0 +1,95 @@ +import { componentTypes, validatorTypes } from '@@ddf'; +import FormSpy from '@data-driven-forms/react-form-renderer/form-spy'; + +const formSchema = (newRecord, groups, userid, isConfirmPasswordEnabled ) => ({ + fields: [ + { + component: componentTypes.SUB_FORM, + id: 'name-wrapper', + name: 'subform-1', + title: __('User Information'), + fields: [ + { + component: componentTypes.TEXT_FIELD, + id: 'name', + name: 'name', + label: __('Full Name'), + maxLength: 50, + // validate: [{ type: validatorTypes.REQUIRED }], + isRequired: newRecord, + isDisabled: userid === 'admin', + }, + { + component: componentTypes.TEXT_FIELD, + id: 'userid', + name: 'userid', + label: __('Username'), + maxLength: 50, + isRequired: newRecord, + isDisabled: userid === 'admin', + }, + { + component: componentTypes.TEXT_FIELD, + id: 'password', + name: 'password', + // type: 'password', + label: __('Password'), + placeholder: newRecord ? ' ' : '●●●●●●●●', + maxLength: 50, + isRequired: newRecord, + isDisabled: !newRecord && isConfirmPasswordEnabled, // Disable when not a new record or isConfirmPasswordEnabled is false + }, + { + component: 'changePassword', + id: 'changePassword', + name: 'changePassword', + label: __('Change Password'), + }, + { + component: componentTypes.TEXT_FIELD, + id: 'verify', + name: 'verify', + type: 'password', + maxLength: 50, + validate: [ + { type: 'same-password', errorText: 'Passwords do not match' } // Add an error message + ], + label: __('Confirm Password'), + isRequired: newRecord, + condition: { + when: 'password', + isNotEmpty: true + } + }, + { + component: componentTypes.TEXT_FIELD, + id: 'email', + name: 'email', + label: __('E-mail Address'), + maxLength: 253, + autoComplete: 'off', + validate: [ + { + type: 'pattern', + pattern: '[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$', + message: 'Invalid email format', + }, + ], + isRequired: newRecord, + }, + { + component: componentTypes.SELECT, + id: 'available_groups', + name: 'available_groups', + label: __('Available Groups'), + placeholder: __('Choose one or more Groups'), + isRequired: newRecord, + // isMulti: true, + options: groups, + }, + ], + }, + ], +}); + +export default formSchema; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index 8ea69b9092e..30fcae19710 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -161,6 +161,7 @@ import StorageServiceForm from '../components/storage-service-form'; import VolumeMappingForm from '../components/volume-mapping-form'; import ZoneForm from '../components/zone-form'; import AnsiblePlayBookEditCatalogForm from '../components/ansible-playbook-edit-catalog-form'; +import SettingsUsersForm from '../components/settings-users-form'; /** * Add component definitions to this file. @@ -331,3 +332,4 @@ ManageIQ.component.addReact('VolumeMappingForm', VolumeMappingForm); ManageIQ.component.addReact('VmCommonRenameForm', VmCommonRenameForm); ManageIQ.component.addReact('ZoneForm', ZoneForm); ManageIQ.component.addReact('AnsiblePlayBookEditCatalogForm', AnsiblePlayBookEditCatalogForm); +ManageIQ.component.addReact('SettingsUsersForm', SettingsUsersForm); diff --git a/app/javascript/spec/settings-users-form/settings-users-form-spec.js b/app/javascript/spec/settings-users-form/settings-users-form-spec.js new file mode 100644 index 00000000000..febcb9f4546 --- /dev/null +++ b/app/javascript/spec/settings-users-form/settings-users-form-spec.js @@ -0,0 +1,35 @@ +import React from 'react'; +import toJson from 'enzyme-to-json'; +import fetchMock from 'fetch-mock'; +import { act } from 'react-dom/test-utils'; + +import { mount } from '../helpers/mountForm'; +import SettingsUsersForm from '../../components/settings-users-form'; + +describe('SettingsUsersForm Component', () => { + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + it('should render a new SettingsUsersForm form', async(done) => { + let wrapper; + await act(async() => { + wrapper = mount(); + }); + wrapper.update(); + expect(toJson(wrapper)).toMatchSnapshot(); + done(); + }); + + it('should render edit SettingsUsersForm', async(done) => { + fetchMock.getOnce('/api/groups?expand=resources/', {}); + let wrapper; + await act(async() => { + wrapper = mount(); + }); + expect(fetchMock.calls()).toHaveLength(1); + expect(fetchMock.called('/api/groups?expand=resources/100')).toBe(true); + expect(toJson(wrapper)).toMatchSnapshot(); + done(); + }); +}); diff --git a/app/views/ops/_rbac_user_details.html.haml b/app/views/ops/_rbac_user_details.html.haml index e65c4aa23b3..791c034d93a 100644 --- a/app/views/ops/_rbac_user_details.html.haml +++ b/app/views/ops/_rbac_user_details.html.haml @@ -1,4 +1,7 @@ - if @edit + = react('SettingsUsersForm', {:recordId => params[:id] || "new"}) + -# = params[:id] + -# = @edit[:user_id] - change_stored_password ||= _("Change") - cancel_password_change ||= _("Cancel") - stored_password_placeholder = "●●●●●●●●"