diff --git a/client/package-lock.json b/client/package-lock.json index a7046b0d..6cd52325 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -25,6 +25,7 @@ "react": "^16.13.1", "react-click-outside": "^3.0.1", "react-dom": "^16.13.1", + "react-hook-form": "^7.54.0", "react-i18next": "^11.7.2", "react-modal": "^3.11.2", "react-popper-tooltip": "^2.11.1", @@ -15570,6 +15571,21 @@ "react": "^16.13.1" } }, + "node_modules/react-hook-form": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", + "integrity": "sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-hot-loader": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.1.tgz", @@ -31540,6 +31556,12 @@ "scheduler": "^0.19.1" } }, + "react-hook-form": { + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", + "integrity": "sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==", + "requires": {} + }, "react-hot-loader": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.1.tgz", diff --git a/client/package.json b/client/package.json index 62c26e19..7d2b4391 100644 --- a/client/package.json +++ b/client/package.json @@ -38,6 +38,7 @@ "react": "^16.13.1", "react-click-outside": "^3.0.1", "react-dom": "^16.13.1", + "react-hook-form": "^7.54.0", "react-i18next": "^11.7.2", "react-modal": "^3.11.2", "react-popper-tooltip": "^2.11.1", diff --git a/client/src/actions/install.ts b/client/src/actions/install.ts index a52b268f..55114d2c 100644 --- a/client/src/actions/install.ts +++ b/client/src/actions/install.ts @@ -27,7 +27,8 @@ export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS'); export const setAllSettings = (values: any) => async (dispatch: any) => { dispatch(setAllSettingsRequest()); try { - const { confirm_password, ...config } = values; + const config = { ...values }; + delete config.confirm_password; await apiClient.setAllSettings(config); dispatch(setAllSettingsSuccess()); @@ -48,7 +49,10 @@ export const checkConfig = (values: any) => async (dispatch: any) => { dispatch(checkConfigRequest()); try { const check = await apiClient.checkConfig(values); - dispatch(checkConfigSuccess(check)); + dispatch(checkConfigSuccess({ + ...values, + check, + })); } catch (error) { dispatch(addErrorToast({ error })); dispatch(checkConfigFailure()); diff --git a/client/src/install/Setup/Auth.tsx b/client/src/install/Setup/Auth.tsx index b9072b4e..ec9933af 100644 --- a/client/src/install/Setup/Auth.tsx +++ b/client/src/install/Setup/Auth.tsx @@ -1,47 +1,46 @@ import React from 'react'; - -import { Field, reduxForm } from 'redux-form'; +import { useForm } from 'react-hook-form'; import { withTranslation, Trans } from 'react-i18next'; import flow from 'lodash/flow'; - +import cn from 'classnames'; import i18n from '../../i18n'; - import Controls from './Controls'; - -import { renderInputField } from '../../helpers/form'; -import { FORM_NAME } from '../../helpers/constants'; import { validatePasswordLength } from '../../helpers/validators'; -const required = (value: any) => { - if (value || value === 0) { - return false; - } - - return form_error_required; -}; - -const validate = (values: any) => { - const errors: { confirm_password?: string } = {}; - - if (values.confirm_password !== values.password) { - errors.confirm_password = i18n.t('form_error_password'); - } - - return errors; -}; - -interface AuthProps { - handleSubmit: (...args: unknown[]) => string; +type Props = { + onAuthSubmit: (...args: unknown[]) => string; pristine: boolean; invalid: boolean; t: (...args: unknown[]) => string; } -const Auth = (props: AuthProps) => { - const { handleSubmit, pristine, invalid, t } = props; +const Auth = (props: Props) => { + const { t, onAuthSubmit } = props; + const { + register, + handleSubmit, + watch, + formState: { errors, isDirty, isValid }, + } = useForm({ + mode: 'onChange', + defaultValues: { + username: '', + password: '', + confirm_password: '', + }, + }); + + const password = watch('password'); + + const validateConfirmPassword = (value: string) => { + if (value !== password) { + return i18n.t('form_error_password'); + } + return undefined; + }; return ( -
+
install_auth_title @@ -55,62 +54,80 @@ const Auth = (props: AuthProps) => { - - + {errors.username && ( +
+ {errors.username.message} +
+ )}
- - + {errors.password && ( +
+ {errors.password.message || i18n.t('form_error_password_length')} +
+ )}
- - + {errors.confirm_password && ( +
+ {errors.confirm_password.message} +
+ )}
- + ); }; export default flow([ withTranslation(), - reduxForm({ - form: FORM_NAME.INSTALL, - destroyOnUnmount: false, - forceUnregisterOnUnmount: true, - validate, - }), ])(Auth); diff --git a/client/src/install/Setup/Settings.tsx b/client/src/install/Setup/Settings.tsx index c884cc89..537f7e63 100644 --- a/client/src/install/Setup/Settings.tsx +++ b/client/src/install/Setup/Settings.tsx @@ -1,30 +1,65 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; - -import { Field, reduxForm, formValueSelector } from 'redux-form'; -import { Trans, withTranslation } from 'react-i18next'; -import flow from 'lodash/flow'; -import i18n, { TFunction } from 'i18next'; +import React, { useEffect, useCallback } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; +import i18n from 'i18next'; +import i18next from 'i18next'; import Controls from './Controls'; - import AddressList from './AddressList'; import { getInterfaceIp } from '../../helpers/helpers'; import { ALL_INTERFACES_IP, - FORM_NAME, ADDRESS_IN_USE_TEXT, PORT_53_FAQ_LINK, STATUS_RESPONSE, STANDARD_DNS_PORT, STANDARD_WEB_PORT, + MAX_PORT, } from '../../helpers/constants'; -import { renderInputField, toNumber } from '../../helpers/form'; -import { validateRequiredValue, validateInstallPort } from '../../helpers/validators'; +import { toNumber } from '../../helpers/form'; +import { validateRequiredValue } from '../../helpers/validators'; import { DhcpInterface } from '../../initialState'; +const validateInstallPort = (value: any) => { + if (value < 1 || value > MAX_PORT) { + return i18next.t('form_error_port'); + } + return undefined; +}; + +type StaticIpType = { + ip: string; + static: string; +}; + +type ConfigType = { + web: { + ip: string; + port?: number; + status: string; + can_autofix: boolean; + }; + dns: { + ip: string; + port?: number; + status: string; + can_autofix: boolean; + }; + staticIp: StaticIpType; +}; + +type Props = { + handleSubmit: (data: any) => void; + handleChange?: (...args: unknown[]) => unknown; + handleFix: (web: any, dns: any, set_static_ip: boolean) => void; + validateForm: (data: any) => void; + config: ConfigType; + interfaces: DhcpInterface[]; + initialValues?: object; +}; + const renderInterfaces = (interfaces: DhcpInterface[]) => Object.values(interfaces).map((option: DhcpInterface) => { const { name, ip_addresses, flags } = option; @@ -43,113 +78,69 @@ const renderInterfaces = (interfaces: DhcpInterface[]) => return null; }); -type Props = { - handleSubmit: (...args: unknown[]) => string; - handleChange?: (...args: unknown[]) => unknown; - handleFix: (...args: unknown[]) => unknown; - validateForm?: (...args: unknown[]) => unknown; - webIp: string; - dnsIp: string; - config: { +const Settings: React.FC = ({ + handleSubmit, + handleFix, + validateForm, + config, + interfaces, +}) => { + const { t } = useTranslation(); + + const defaultValues = { web: { - status: string; - can_autofix: boolean; - }; + ip: config.web.ip || ALL_INTERFACES_IP, + port: config.web.port || STANDARD_WEB_PORT, + }, dns: { - status: string; - can_autofix: boolean; - }; - staticIp: { - ip: string; - static: string; - }; + ip: config.dns.ip || ALL_INTERFACES_IP, + port: config.dns.port || STANDARD_DNS_PORT, + }, }; - webPort?: number; - dnsPort?: number; - interfaces: DhcpInterface[]; - invalid: boolean; - initialValues?: object; - t: TFunction; -}; -class Settings extends Component { - componentDidMount() { - const { webIp, webPort, dnsIp, dnsPort } = this.props; + const { + control, + watch, + handleSubmit: reactHookFormSubmit, + formState: { isValid, errors }, + } = useForm({ + defaultValues, + mode: 'onChange', + }); - this.props.validateForm({ + const watchFields = watch(); + + const { status: webStatus, can_autofix: isWebFixAvailable } = config.web; + const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns; + const { staticIp } = config; + + const webIpVal = watch("web.ip"); + const webPortVal = watch("web.port"); + const dnsIpVal = watch("dns.ip"); + const dnsPortVal = watch("dns.port"); + + useEffect(() => { + validateForm({ web: { - ip: webIp, - port: webPort, + ip: webIpVal, + port: webPortVal, }, dns: { - ip: dnsIp, - port: dnsPort, + ip: dnsIpVal, + port: dnsPortVal, }, }); - } - - getStaticIpMessage = (staticIp: { ip: string; static: string }) => { - const { static: status, ip } = staticIp; - - switch (status) { - case STATUS_RESPONSE.NO: { - return ( - <> -
- text]}> - install_static_configure - -
- - - - ); - } - case STATUS_RESPONSE.ERROR: { - return ( -
- install_static_error -
- ); - } - case STATUS_RESPONSE.YES: { - return ( -
- install_static_ok -
- ); - } - default: - return null; - } - }; - - handleAutofix = (type: any) => { - const { - webIp, - - webPort, - - dnsIp, - - dnsPort, - - handleFix, - } = this.props; + }, [webIpVal, webPortVal, dnsIpVal, dnsPortVal]); + const handleAutofix = (type: string) => { const web = { - ip: webIp, - port: webPort, + ip: watchFields.web?.ip, + port: watchFields.web?.port, autofix: false, }; const dns = { - ip: dnsIp, - port: dnsPort, + ip: watchFields.dns?.ip, + port: watchFields.dns?.port, autofix: false, }; const set_static_ip = false; @@ -163,276 +154,315 @@ class Settings extends Component { handleFix(web, dns, set_static_ip); }; - handleStaticIp = (ip: any) => { - const { - webIp, - - webPort, - - dnsIp, - - dnsPort, - - handleFix, - } = this.props; - + const handleStaticIp = (ip: string) => { const web = { - ip: webIp, - port: webPort, + ip: watchFields.web?.ip, + port: watchFields.web?.port, autofix: false, }; const dns = { - ip: dnsIp, - port: dnsPort, + ip: watchFields.dns?.ip, + port: watchFields.dns?.port, autofix: false, }; const set_static_ip = true; - if (window.confirm(this.props.t('confirm_static_ip', { ip }))) { + if (window.confirm(t('confirm_static_ip', { ip }))) { handleFix(web, dns, set_static_ip); } }; - render() { - const { - handleSubmit, + const getStaticIpMessage = useCallback((staticIp: StaticIpType) => { + const { static: status, ip } = staticIp; - handleChange, + switch (status) { + case STATUS_RESPONSE.NO: + return ( + <> +
+ text]}> + install_static_configure + +
- webIp, + + + ); + case STATUS_RESPONSE.ERROR: + return ( +
+ install_static_error +
+ ); + case STATUS_RESPONSE.YES: + return ( +
+ install_static_ok +
+ ); + default: + return null; + } + }, [handleStaticIp]); - webPort, + const onSubmit = (data: any) => { + validateForm(data); + handleSubmit(data); + }; - dnsIp, + return ( +
+
+
+ install_settings_title +
- dnsPort, - - interfaces, - - invalid, - - config, - - t, - } = this.props; - const { status: webStatus, can_autofix: isWebFixAvailable } = config.web; - const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns; - const { staticIp } = config; - - return ( - -
-
- install_settings_title +
+
+
+ + ( + + )} + /> +
-
-
-
- - - - - {renderInterfaces(interfaces)} - -
+
+
+ + ( + { + const val = toNumber(e.target.value); + field.onChange(val); + }} + /> + )} + /> + {errors.web?.port && ( +
+ {errors.web.port.message} +
+ )}
+
-
-
- - - +
+ {webStatus && ( +
+ {webStatus} + {isWebFixAvailable && ( + + )}
-
+ )} -
- {webStatus && ( +
+
+
+ +
+ install_settings_interface_link + +
+ +
+
+
+ +
+
+ install_settings_dns +
+ +
+
+
+ + ( + + )} + /> +
+
+ +
+
+ + ( + { + const val = toNumber(e.target.value); + field.onChange(val); + }} + /> + )} + /> + {errors.dns?.port.message && ( +
+ {t(errors.dns.port.message)} +
+ )} +
+
+ +
+ {dnsStatus && ( + <>
- {webStatus} - {isWebFixAvailable && ( + {dnsStatus} + {isDnsFixAvailable && ( )}
- )} - -
-
-
- -
- install_settings_interface_link - -
- -
-
-
- -
-
- install_settings_dns -
- -
-
-
- - - - - {renderInterfaces(interfaces)} - -
-
- -
-
- - - -
-
- -
- {dnsStatus && ( - <> -
- {dnsStatus} - {isDnsFixAvailable && ( - - )} + {isDnsFixAvailable && ( +
+

+ autofix_warning_text +

+ text]}>autofix_warning_list +

+ autofix_warning_result +

- {isDnsFixAvailable && ( -
-

- autofix_warning_text -

- - text]}>autofix_warning_list - -

- autofix_warning_result -

-
- )} - - )} - {dnsPort === STANDARD_DNS_PORT && - !isDnsFixAvailable && - dnsStatus.includes(ADDRESS_IN_USE_TEXT) && ( - - link - , - ]}> - port_53_faq_link - )} + + )} + {watchFields.dns?.port === STANDARD_DNS_PORT && + !isDnsFixAvailable && + dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && ( + + link + , + ]} + > + port_53_faq_link + + )} -
-
-
- -
- install_settings_dns_desc - -
- -
+
-
-
- static_ip -
+
+ install_settings_dns_desc -
- static_ip_desc +
+
+
+
- {this.getStaticIpMessage(staticIp)} +
+
+ static_ip
- - - ); - } -} +
+ static_ip_desc +
-const selector = formValueSelector(FORM_NAME.INSTALL); + {getStaticIpMessage(staticIp)} +
-const SettingsForm = connect((state) => { - const webIp = selector(state, 'web.ip'); - const webPort = selector(state, 'web.port'); - const dnsIp = selector(state, 'dns.ip'); - const dnsPort = selector(state, 'dns.port'); + + + ); +}; - return { - webIp, - webPort, - dnsIp, - dnsPort, - }; -})(Settings); - -export default flow([ - withTranslation(), - reduxForm({ - form: FORM_NAME.INSTALL, - destroyOnUnmount: false, - forceUnregisterOnUnmount: true, - }), -])(SettingsForm); +export default Settings; diff --git a/client/src/install/Setup/index.tsx b/client/src/install/Setup/index.tsx index dbfa7573..1e145f17 100644 --- a/client/src/install/Setup/index.tsx +++ b/client/src/install/Setup/index.tsx @@ -1,101 +1,77 @@ -import React, { Component, Fragment } from 'react'; -import { connect } from 'react-redux'; +import React, { useEffect, Fragment } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import debounce from 'lodash/debounce'; import * as actionCreators from '../../actions/install'; import { getWebAddress } from '../../helpers/helpers'; -import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../helpers/constants'; +import { INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../helpers/constants'; import Loading from '../../components/ui/Loading'; - import Greeting from './Greeting'; - import Settings from './Settings'; - -import Auth from './Auth'; - import Devices from './Devices'; - import Submit from './Submit'; - import Progress from './Progress'; - import Toasts from '../../components/Toasts'; - import Footer from '../../components/ui/Footer'; - import Icons from '../../components/ui/Icons'; - import { Logo } from '../../components/ui/svg/logo'; import './Setup.css'; import '../../components/ui/Tabler.css'; +import Auth from './Auth'; -interface SetupProps { - getDefaultAddresses: (...args: unknown[]) => unknown; - setAllSettings: (...args: unknown[]) => unknown; - checkConfig: (...args: unknown[]) => unknown; - nextStep: (...args: unknown[]) => unknown; - prevStep: (...args: unknown[]) => unknown; - install: { - step: number; - processingDefault: boolean; - web; - dns; - staticIp; - interfaces; - }; - step?: number; - web?: object; - dns?: object; -} +const Setup = () => { + const dispatch = useDispatch(); -class Setup extends Component { - componentDidMount() { - this.props.getDefaultAddresses(); - } + const install = useSelector((state: any) => state.install); + const { processingDefault, step, web, dns, staticIp, interfaces } = install; - handleFormSubmit = (values: any) => { - const { staticIp, ...config } = values; + useEffect(() => { + dispatch(actionCreators.getDefaultAddresses()); + }, []); - this.props.setAllSettings(config); + const handleFormSubmit = (values: any) => { + const config = { ...values }; + delete config.staticIp; + + if (web.port && dns.port) { + dispatch(actionCreators.setAllSettings({ + web, + dns, + ...config, + })); + } }; - handleFormChange = debounce((values) => { + const checkConfig = debounce((values) => { const { web, dns } = values; + if (values && web.port && dns.port) { - this.props.checkConfig({ web, dns, set_static_ip: false }); + dispatch(actionCreators.checkConfig({ web, dns, set_static_ip: false })); } }, DEBOUNCE_TIMEOUT); - handleFix = (web: any, dns: any, set_static_ip: any) => { - this.props.checkConfig({ web, dns, set_static_ip }); + const handleFix = (web: any, dns: any, set_static_ip: any) => { + dispatch(actionCreators.checkConfig({ web, dns, set_static_ip })); }; - openDashboard = (ip: any, port: any) => { + const openDashboard = (ip: any, port: any) => { let address = getWebAddress(ip, port); - if (ip === ALL_INTERFACES_IP) { address = getWebAddress(window.location.hostname, port); } - window.location.replace(address); }; - nextStep = () => { - if (this.props.install.step < INSTALL_TOTAL_STEPS) { - this.props.nextStep(); + const handleNextStep = () => { + if (step < INSTALL_TOTAL_STEPS) { + dispatch(actionCreators.nextStep()); } }; - prevStep = () => { - if (this.props.install.step > INSTALL_FIRST_STEP) { - this.props.prevStep(); - } - }; - - renderPage(step: any, config: any, interfaces: any) { + const renderPage = (step: any, config: any, interfaces: any) => { switch (step) { case 1: return ; @@ -105,55 +81,43 @@ class Setup extends Component { config={config} initialValues={config} interfaces={interfaces} - onSubmit={this.nextStep} - onChange={this.handleFormChange} - validateForm={this.handleFormChange} - handleFix={this.handleFix} + handleSubmit={handleNextStep} + validateForm={checkConfig} + handleFix={handleFix} /> ); case 3: - return ; + return ; case 4: return ; case 5: - return ; + return ; default: return false; } + }; + + if (processingDefault) { + return ; } - render() { - const { processingDefault, step, web, dns, staticIp, interfaces } = this.props.install; + return ( + <> +
+
+ + {renderPage(step, { web, dns, staticIp }, interfaces)} + +
+
- return ( - - {processingDefault && } - {!processingDefault && ( - -
-
- - {this.renderPage(step, { web, dns, staticIp }, interfaces)} - -
-
+