cleanup forms
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
import React, { ComponentProps, forwardRef, ReactNode } from 'react';
|
import React, { ComponentProps, forwardRef, ReactNode } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface Props extends ComponentProps<'input'> {
|
type Props = ComponentProps<'input'> & {
|
||||||
label?: string;
|
label?: string;
|
||||||
leftAddon?: ReactNode;
|
leftAddon?: ReactNode;
|
||||||
rightAddon?: ReactNode;
|
rightAddon?: ReactNode;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, Props>(
|
export const Input = forwardRef<HTMLInputElement, Props>(
|
||||||
({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => (
|
({ name, label, className, leftAddon, rightAddon, error, ...rest }, ref) => (
|
||||||
@@ -18,10 +18,10 @@ export const Input = forwardRef<HTMLInputElement, Props>(
|
|||||||
)}
|
)}
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
{leftAddon && <div>{leftAddon}</div>}
|
{leftAddon && <div>{leftAddon}</div>}
|
||||||
<input className={clsx('form-control', className)} ref={ref} {...rest} />
|
<input className={clsx('form-control', { 'is-invalid': !!error }, className)} ref={ref} {...rest} />
|
||||||
{rightAddon && <div>{rightAddon}</div>}
|
{rightAddon && <div>{rightAddon}</div>}
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="form__message form__message--error">{error}</div>}
|
{error && <div className="form__message form__message--error mt-1">{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
27
client/src/components/ui/Controls/Select.tsx
Normal file
27
client/src/components/ui/Controls/Select.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { ComponentProps, forwardRef } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
type SelectProps = ComponentProps<'select'> & {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
|
({ name, label, className, error, children, ...rest }, ref) => (
|
||||||
|
<div className={clsx('form-group', { 'has-error': !!error })}>
|
||||||
|
{label && (
|
||||||
|
<label className="form__label" htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className="input-group">
|
||||||
|
<select className={clsx('form-control custom-select', className)} ref={ref} {...rest}>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{error && <div className="form__message form__message--error mt-1">{error}</div>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Select.displayName = 'Select';
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { ComponentProps, forwardRef } from 'react';
|
import React, { ComponentProps, forwardRef } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface Props extends ComponentProps<'textarea'> {
|
type Props = ComponentProps<'textarea'> & {
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ name, label, className, error, ...rest }, ref) => (
|
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ name, label, className, error, ...rest }, ref) => (
|
||||||
<div className={clsx('form-group', { 'has-error': !!error })}>
|
<div className={clsx('form-group', { 'has-error': !!error })}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
validatePort,
|
validatePort,
|
||||||
validateIsSafePort,
|
validateIsSafePort,
|
||||||
} from '../../../helpers/validators';
|
} from '../../../helpers/validators';
|
||||||
|
import { Input } from '../Controls/Input';
|
||||||
|
|
||||||
const getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => {
|
const getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => {
|
||||||
if (!host || invalid) {
|
if (!host || invalid) {
|
||||||
@@ -23,7 +24,7 @@ const getDownloadLink = (host: string, clientId: string, protocol: string, inval
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkParams: { host: string, client_id?: string } = { host };
|
const linkParams: { host: string; client_id?: string } = { host };
|
||||||
|
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
linkParams.client_id = clientId;
|
linkParams.client_id = clientId;
|
||||||
@@ -33,8 +34,7 @@ const getDownloadLink = (host: string, clientId: string, protocol: string, inval
|
|||||||
<a
|
<a
|
||||||
href={getPathWithQueryString(protocol, linkParams)}
|
href={getPathWithQueryString(protocol, linkParams)}
|
||||||
className={cn('btn btn-success btn-standard btn-large')}
|
className={cn('btn btn-success btn-standard btn-large')}
|
||||||
download
|
download>
|
||||||
>
|
|
||||||
{i18next.t('download_mobileconfig')}
|
{i18next.t('download_mobileconfig')}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@@ -51,24 +51,32 @@ type FormValues = {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialValues?: FormValues;
|
initialValues?: FormValues;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const defaultFormValues = {
|
||||||
|
host: '',
|
||||||
|
clientId: '',
|
||||||
|
protocol: MOBILE_CONFIG_LINKS.DOT,
|
||||||
|
port: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export const MobileConfigForm = ({ initialValues }: Props) => {
|
export const MobileConfigForm = ({ initialValues }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors, isValid },
|
control,
|
||||||
} = useForm<FormValues>({
|
formState: { isValid },
|
||||||
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
host: initialValues?.host || '',
|
...defaultFormValues,
|
||||||
clientId: initialValues?.clientId || '',
|
...initialValues,
|
||||||
protocol: initialValues?.protocol || MOBILE_CONFIG_LINKS.DOT,
|
|
||||||
port: initialValues?.port || undefined,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,50 +99,46 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
|
|||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<label htmlFor="host" className="form__label">
|
<Controller
|
||||||
{i18next.t('dhcp_table_hostname')}
|
name="host"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{ validate: validateServerName }}
|
||||||
<input
|
render={({ field, fieldState }) => (
|
||||||
id="host"
|
<Input
|
||||||
type="text"
|
{...field}
|
||||||
className="form-control"
|
type="text"
|
||||||
placeholder={i18next.t('form_enter_hostname')}
|
label={t('dhcp_table_hostname')}
|
||||||
{...register('host', {
|
placeholder={t('form_enter_hostname')}
|
||||||
validate: validateServerName,
|
error={fieldState.error?.message}
|
||||||
})}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.host && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.host.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{protocol === MOBILE_CONFIG_LINKS.DOH && (
|
{protocol === MOBILE_CONFIG_LINKS.DOH && (
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<label htmlFor="port" className="form__label">
|
<Controller
|
||||||
{i18next.t('encryption_https')}
|
name="port"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{
|
||||||
<input
|
|
||||||
id="port"
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
placeholder={i18next.t('encryption_https')}
|
|
||||||
{...register('port', {
|
|
||||||
setValueAs: (val) => toNumber(val),
|
|
||||||
validate: {
|
validate: {
|
||||||
range: (value) => validatePort(value) || true,
|
range: (value) => validatePort(value) || true,
|
||||||
safety: (value) => validateIsSafePort(value) || true,
|
safety: (value) => validateIsSafePort(value) || true,
|
||||||
},
|
},
|
||||||
})}
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
label={t('encryption_https')}
|
||||||
|
placeholder={t('encryption_https')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{errors.port && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.port.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -149,21 +153,21 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
|
|||||||
<Trans components={{ a: githubLink }}>client_id_desc</Trans>
|
<Trans components={{ a: githubLink }}>client_id_desc</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<Controller
|
||||||
id="clientId"
|
name="clientId"
|
||||||
type="text"
|
control={control}
|
||||||
className="form-control"
|
rules={{
|
||||||
placeholder={i18next.t('client_id_placeholder')}
|
|
||||||
{...register('clientId', {
|
|
||||||
validate: validateConfigClientId,
|
validate: validateConfigClientId,
|
||||||
})}
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
placeholder={t('client_id_placeholder')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{errors.clientId && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.clientId.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
@@ -171,11 +175,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
|
|||||||
{i18next.t('protocol')}
|
{i18next.t('protocol')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<select
|
<select id="protocol" className="form-control" {...register('protocol')}>
|
||||||
id="protocol"
|
|
||||||
className="form-control"
|
|
||||||
{...register('protocol')}
|
|
||||||
>
|
|
||||||
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
|
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
|
||||||
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
|
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export const STANDARD_WEB_PORT = 80;
|
|||||||
export const STANDARD_HTTPS_PORT = 443;
|
export const STANDARD_HTTPS_PORT = 443;
|
||||||
export const DNS_OVER_TLS_PORT = 853;
|
export const DNS_OVER_TLS_PORT = 853;
|
||||||
export const DNS_OVER_QUIC_PORT = 853;
|
export const DNS_OVER_QUIC_PORT = 853;
|
||||||
|
export const MIN_PORT = 1;
|
||||||
export const MAX_PORT = 65535;
|
export const MAX_PORT = 65535;
|
||||||
|
|
||||||
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { withTranslation, Trans } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import flow from 'lodash/flow';
|
|
||||||
import cn from 'classnames';
|
|
||||||
import i18n from '../../i18n';
|
|
||||||
import Controls from './Controls';
|
import Controls from './Controls';
|
||||||
import { validatePasswordLength } from '../../helpers/validators';
|
import { validatePasswordLength, validateRequiredValue } from '../../helpers/validators';
|
||||||
|
import { Input } from '../../components/ui/Controls/Input';
|
||||||
|
|
||||||
|
type AuthFormValues = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
confirm_password: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onAuthSubmit: (...args: unknown[]) => string;
|
onAuthSubmit: (values: AuthFormValues) => void;
|
||||||
pristine: boolean;
|
};
|
||||||
invalid: boolean;
|
|
||||||
t: (...args: unknown[]) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Auth = (props: Props) => {
|
export const Auth = ({ onAuthSubmit }: Props) => {
|
||||||
const { t, onAuthSubmit } = props;
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors, isDirty, isValid },
|
control,
|
||||||
} = useForm({
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm<AuthFormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -34,7 +35,7 @@ const Auth = (props: Props) => {
|
|||||||
|
|
||||||
const validateConfirmPassword = (value: string) => {
|
const validateConfirmPassword = (value: string) => {
|
||||||
if (value !== password) {
|
if (value !== password) {
|
||||||
return i18n.t('form_error_password');
|
return t('form_error_password');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -51,74 +52,71 @@ const Auth = (props: Props) => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>
|
<Controller
|
||||||
<Trans>install_auth_username</Trans>
|
name="username"
|
||||||
</label>
|
control={control}
|
||||||
<input
|
rules={{ validate: validateRequiredValue }}
|
||||||
{...register('username', { required: t('form_error_required') })}
|
render={({ field, fieldState }) => (
|
||||||
type="text"
|
<Input
|
||||||
className={cn('form-control', { 'is-invalid': errors.username })}
|
{...field}
|
||||||
placeholder={t('install_auth_username_enter')}
|
type="text"
|
||||||
autoComplete="username"
|
label={t('install_auth_username')}
|
||||||
|
placeholder={t('install_auth_username_enter')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.username && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.username.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>
|
<Controller
|
||||||
<Trans>install_auth_password</Trans>
|
name="password"
|
||||||
</label>
|
control={control}
|
||||||
<input
|
rules={{
|
||||||
{...register('password', {
|
validate: {
|
||||||
required: t('form_error_required'),
|
required: validateRequiredValue,
|
||||||
validate: validatePasswordLength,
|
passwordLength: validatePasswordLength,
|
||||||
})}
|
},
|
||||||
type="password"
|
}}
|
||||||
className={cn('form-control', { 'is-invalid': errors.password })}
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('install_auth_password_enter')}
|
<Input
|
||||||
autoComplete="new-password"
|
{...field}
|
||||||
|
type="password"
|
||||||
|
label={t('install_auth_password')}
|
||||||
|
placeholder={t('install_auth_password_enter')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.password && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.password.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>
|
<Controller
|
||||||
<Trans>install_auth_confirm</Trans>
|
name="confirm_password"
|
||||||
</label>
|
control={control}
|
||||||
<input
|
rules={{
|
||||||
{...register('confirm_password', {
|
validate: {
|
||||||
required: t('form_error_required'),
|
required: validateRequiredValue,
|
||||||
validate: validateConfirmPassword,
|
confirmPassword: validateConfirmPassword,
|
||||||
})}
|
},
|
||||||
type="password"
|
}}
|
||||||
className={cn('form-control', { 'is-invalid': errors.confirm_password })}
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('install_auth_confirm')}
|
<Input
|
||||||
autoComplete="new-password"
|
{...field}
|
||||||
|
type="password"
|
||||||
|
label={t('install_auth_confirm')}
|
||||||
|
placeholder={t('install_auth_confirm')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.confirm_password && (
|
|
||||||
<div className="invalid-feedback">
|
|
||||||
{errors.confirm_password.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Controls
|
<Controls isDirty={isDirty} isValid={isValid} />
|
||||||
isDirty={isDirty}
|
|
||||||
isValid={isValid}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
])(Auth);
|
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ import {
|
|||||||
STANDARD_DNS_PORT,
|
STANDARD_DNS_PORT,
|
||||||
STANDARD_WEB_PORT,
|
STANDARD_WEB_PORT,
|
||||||
MAX_PORT,
|
MAX_PORT,
|
||||||
|
MIN_PORT,
|
||||||
} from '../../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
|
|
||||||
import { toNumber } from '../../helpers/form';
|
|
||||||
import { validateRequiredValue } from '../../helpers/validators';
|
import { validateRequiredValue } from '../../helpers/validators';
|
||||||
import { DhcpInterface } from '../../initialState';
|
import { DhcpInterface } from '../../initialState';
|
||||||
|
import { Input } from '../../components/ui/Controls/Input';
|
||||||
|
import { Select } from '../../components/ui/Controls/Select';
|
||||||
|
import { toNumber } from '../../helpers/form';
|
||||||
|
|
||||||
const validateInstallPort = (value: any) => {
|
const validateInstallPort = (value: number) => {
|
||||||
if (value < 1 || value > MAX_PORT) {
|
if (value < MIN_PORT || value > MAX_PORT) {
|
||||||
return i18n.t('form_error_port');
|
return i18n.t('form_error_port');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -77,13 +80,7 @@ const renderInterfaces = (interfaces: DhcpInterface[]) =>
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const Settings: React.FC<Props> = ({
|
export const Settings = ({ handleSubmit, handleFix, validateForm, config, interfaces }: Props) => {
|
||||||
handleSubmit,
|
|
||||||
handleFix,
|
|
||||||
validateForm,
|
|
||||||
config,
|
|
||||||
interfaces,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
@@ -101,7 +98,7 @@ const Settings: React.FC<Props> = ({
|
|||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
handleSubmit: reactHookFormSubmit,
|
handleSubmit: reactHookFormSubmit,
|
||||||
formState: { isValid, errors },
|
formState: { isValid },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
@@ -113,12 +110,16 @@ const Settings: React.FC<Props> = ({
|
|||||||
const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns;
|
const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns;
|
||||||
const { staticIp } = config;
|
const { staticIp } = config;
|
||||||
|
|
||||||
const webIpVal = watch("web.ip");
|
const webIpVal = watch('web.ip');
|
||||||
const webPortVal = watch("web.port");
|
const webPortVal = watch('web.port');
|
||||||
const dnsIpVal = watch("dns.ip");
|
const dnsIpVal = watch('dns.ip');
|
||||||
const dnsPortVal = watch("dns.port");
|
const dnsPortVal = watch('dns.port');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isValid || validateInstallPort(webPortVal) || validateInstallPort(dnsPortVal)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
validateForm({
|
validateForm({
|
||||||
web: {
|
web: {
|
||||||
ip: webIpVal,
|
ip: webIpVal,
|
||||||
@@ -171,44 +172,46 @@ const Settings: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStaticIpMessage = useCallback((staticIp: StaticIpType) => {
|
const getStaticIpMessage = useCallback(
|
||||||
const { static: status, ip } = staticIp;
|
(staticIp: StaticIpType) => {
|
||||||
|
const { static: status, ip } = staticIp;
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case STATUS_RESPONSE.NO:
|
case STATUS_RESPONSE.NO:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
|
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
|
||||||
install_static_configure
|
install_static_configure
|
||||||
</Trans>
|
</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
onClick={() => handleStaticIp(ip)}>
|
||||||
|
<Trans>set_static_ip</Trans>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case STATUS_RESPONSE.ERROR:
|
||||||
|
return (
|
||||||
|
<div className="text-danger">
|
||||||
|
<Trans>install_static_error</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<button
|
case STATUS_RESPONSE.YES:
|
||||||
type="button"
|
return (
|
||||||
className="btn btn-outline-primary btn-sm"
|
<div className="text-success">
|
||||||
onClick={() => handleStaticIp(ip)}
|
<Trans>install_static_ok</Trans>
|
||||||
>
|
</div>
|
||||||
<Trans>set_static_ip</Trans>
|
);
|
||||||
</button>
|
default:
|
||||||
</>
|
return null;
|
||||||
);
|
}
|
||||||
case STATUS_RESPONSE.ERROR:
|
},
|
||||||
return (
|
[handleStaticIp],
|
||||||
<div className="text-danger">
|
);
|
||||||
<Trans>install_static_error</Trans>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case STATUS_RESPONSE.YES:
|
|
||||||
return (
|
|
||||||
<div className="text-success">
|
|
||||||
<Trans>install_static_ok</Trans>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, [handleStaticIp]);
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: any) => {
|
||||||
validateForm(data);
|
validateForm(data);
|
||||||
@@ -232,17 +235,12 @@ const Settings: React.FC<Props> = ({
|
|||||||
name="web.ip"
|
name="web.ip"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<select
|
<Select {...field}>
|
||||||
{...field}
|
|
||||||
className="form-control custom-select"
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(e);
|
|
||||||
}}>
|
|
||||||
<option value={ALL_INTERFACES_IP}>
|
<option value={ALL_INTERFACES_IP}>
|
||||||
{t('install_settings_all_interfaces')}
|
{t('install_settings_all_interfaces')}
|
||||||
</option>
|
</option>
|
||||||
{renderInterfaces(interfaces)}
|
{renderInterfaces(interfaces)}
|
||||||
</select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,29 +255,24 @@ const Settings: React.FC<Props> = ({
|
|||||||
name="web.port"
|
name="web.port"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
required: t('form_error_required'),
|
|
||||||
validate: {
|
validate: {
|
||||||
|
required: validateRequiredValue,
|
||||||
installPort: validateInstallPort,
|
installPort: validateInstallPort,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
render={({ field }) => (
|
render={({ field, fieldState }) => (
|
||||||
<input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
|
||||||
placeholder={STANDARD_WEB_PORT.toString()}
|
placeholder={STANDARD_WEB_PORT.toString()}
|
||||||
|
error={fieldState.error?.message}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = toNumber(e.target.value);
|
const { value } = e.target;
|
||||||
field.onChange(val);
|
field.onChange(toNumber(value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.web?.port && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{errors.web.port.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -291,8 +284,7 @@ const Settings: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary btn-sm ml-2"
|
className="btn btn-secondary btn-sm ml-2"
|
||||||
onClick={() => handleAutofix('web')}
|
onClick={() => handleAutofix('web')}>
|
||||||
>
|
|
||||||
<Trans>fix</Trans>
|
<Trans>fix</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -331,17 +323,12 @@ const Settings: React.FC<Props> = ({
|
|||||||
name="dns.ip"
|
name="dns.ip"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<select
|
<Select {...field}>
|
||||||
{...field}
|
|
||||||
className="form-control custom-select"
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(e);
|
|
||||||
}}>
|
|
||||||
<option value={ALL_INTERFACES_IP}>
|
<option value={ALL_INTERFACES_IP}>
|
||||||
{t('install_settings_all_interfaces')}
|
{t('install_settings_all_interfaces')}
|
||||||
</option>
|
</option>
|
||||||
{renderInterfaces(interfaces)}
|
{renderInterfaces(interfaces)}
|
||||||
</select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,24 +349,19 @@ const Settings: React.FC<Props> = ({
|
|||||||
installPort: validateInstallPort,
|
installPort: validateInstallPort,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
render={({ field }) => (
|
render={({ field, fieldState }) => (
|
||||||
<input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
error={fieldState.error?.message}
|
||||||
placeholder={STANDARD_WEB_PORT.toString()}
|
placeholder={STANDARD_WEB_PORT.toString()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = toNumber(e.target.value);
|
const { value } = e.target;
|
||||||
field.onChange(val);
|
field.onChange(toNumber(value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.dns?.port.message && (
|
|
||||||
<div className="form__message form__message--error">
|
|
||||||
{t(errors.dns.port.message)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -415,16 +397,10 @@ const Settings: React.FC<Props> = ({
|
|||||||
dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && (
|
dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && (
|
||||||
<Trans
|
<Trans
|
||||||
components={[
|
components={[
|
||||||
<a
|
<a href={PORT_53_FAQ_LINK} key="0" target="_blank" rel="noopener noreferrer">
|
||||||
href={PORT_53_FAQ_LINK}
|
|
||||||
key="0"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
link
|
link
|
||||||
</a>,
|
</a>,
|
||||||
]}
|
]}>
|
||||||
>
|
|
||||||
port_53_faq_link
|
port_53_faq_link
|
||||||
</Trans>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
@@ -463,5 +439,3 @@ const Settings: React.FC<Props> = ({
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import { INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../
|
|||||||
|
|
||||||
import Loading from '../../components/ui/Loading';
|
import Loading from '../../components/ui/Loading';
|
||||||
import Greeting from './Greeting';
|
import Greeting from './Greeting';
|
||||||
import Settings from './Settings';
|
import { Settings } from './Settings';
|
||||||
import Devices from './Devices';
|
import Devices from './Devices';
|
||||||
import Submit from './Submit';
|
import Submit from './Submit';
|
||||||
import Progress from './Progress';
|
import Progress from './Progress';
|
||||||
|
import { Auth } from './Auth';
|
||||||
import Toasts from '../../components/Toasts';
|
import Toasts from '../../components/Toasts';
|
||||||
import Footer from '../../components/ui/Footer';
|
import Footer from '../../components/ui/Footer';
|
||||||
import Icons from '../../components/ui/Icons';
|
import Icons from '../../components/ui/Icons';
|
||||||
@@ -20,7 +21,6 @@ import { Logo } from '../../components/ui/svg/logo';
|
|||||||
|
|
||||||
import './Setup.css';
|
import './Setup.css';
|
||||||
import '../../components/ui/Tabler.css';
|
import '../../components/ui/Tabler.css';
|
||||||
import Auth from './Auth';
|
|
||||||
|
|
||||||
const Setup = () => {
|
const Setup = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -37,11 +37,13 @@ const Setup = () => {
|
|||||||
delete config.staticIp;
|
delete config.staticIp;
|
||||||
|
|
||||||
if (web.port && dns.port) {
|
if (web.port && dns.port) {
|
||||||
dispatch(actionCreators.setAllSettings({
|
dispatch(
|
||||||
web,
|
actionCreators.setAllSettings({
|
||||||
dns,
|
web,
|
||||||
...config,
|
dns,
|
||||||
}));
|
...config,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,4 +122,4 @@ const Setup = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Setup;
|
export default Setup;
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Input } from '../../components/ui/Controls/Input';
|
||||||
|
import { validateRequiredValue } from '../../helpers/validators';
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type LoginFormProps = {
|
type LoginFormProps = {
|
||||||
onSubmit: (data: FormValues) => void;
|
onSubmit: (data: FormValues) => void;
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isValid },
|
control,
|
||||||
|
formState: { isValid },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -30,58 +32,46 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)} className="card">
|
<form onSubmit={handleSubmit(onSubmit)} className="card">
|
||||||
<div className="card-body p-6">
|
<div className="card-body p-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label className="form__label" htmlFor="username">
|
<Controller
|
||||||
{t('username_label')}
|
name="username"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{ validate: validateRequiredValue }}
|
||||||
<input
|
render={({ field, fieldState }) => (
|
||||||
id="username"
|
<Input
|
||||||
type="text"
|
{...field}
|
||||||
className="form-control"
|
type="text"
|
||||||
placeholder={t('username_placeholder')}
|
label={t('username_label')}
|
||||||
autoComplete="username"
|
placeholder={t('username_placeholder')}
|
||||||
autoCapitalize="none"
|
error={fieldState.error?.message}
|
||||||
disabled={processing}
|
autoComplete="username"
|
||||||
{...register('username', {
|
autoCapitalize="none"
|
||||||
required: t('form_error_required'),
|
disabled={processing}
|
||||||
})}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.username && (
|
|
||||||
<span className="form__message form__message--error">
|
|
||||||
{errors.username.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label className="form__label" htmlFor="password">
|
<Controller
|
||||||
{t('password_label')}
|
name="password"
|
||||||
</label>
|
control={control}
|
||||||
|
rules={{ validate: validateRequiredValue }}
|
||||||
<input
|
render={({ field, fieldState }) => (
|
||||||
id="password"
|
<Input
|
||||||
type="password"
|
{...field}
|
||||||
className="form-control"
|
type="password"
|
||||||
placeholder={t('password_placeholder')}
|
label={t('username_label')}
|
||||||
autoComplete="current-password"
|
placeholder={t('password_placeholder')}
|
||||||
disabled={processing}
|
error={fieldState.error?.message}
|
||||||
{...register('password', {
|
autoComplete="current-password"
|
||||||
required: t('form_error_required'),
|
disabled={processing}
|
||||||
})}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.password && (
|
|
||||||
<span className="form__message form__message--error">
|
|
||||||
{errors.password.message}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<button
|
<button type="submit" className="btn btn-success btn-block" disabled={processing || !isValid}>
|
||||||
type="submit"
|
|
||||||
className="btn btn-success btn-block"
|
|
||||||
disabled={processing || !isValid}
|
|
||||||
>
|
|
||||||
{t('sign_in')}
|
{t('sign_in')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,4 +80,4 @@ const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Form;
|
export default Form;
|
||||||
|
|||||||
Reference in New Issue
Block a user