Pull request 2322: ADG-9415

Merge in DNS/adguard-home from ADG-9415 to master

Squashed commit of the following:

commit 76bf99499a
Merge: 29529970a 0389515ee
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Feb 26 18:31:41 2025 +0300

    Merge branch 'master' into ADG-9415

commit 29529970a3
Merge: b49790daf 782a1a982
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Feb 24 15:44:38 2025 +0300

    Merge branch 'master' into ADG-9415

commit b49790daf8
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 24 15:30:18 2025 +0300

    fix default lease duration value

commit cb307472ec
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 24 10:35:26 2025 +0300

    fix default response status

commit 115e743e1a
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 24 10:32:46 2025 +0300

    fix upstream description

commit 26b0eddaca
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 18 17:40:41 2025 +0300

    use const for test config file

commit 58faa7c537
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 18 17:31:04 2025 +0300

    fix install config

commit 0a3346d911
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Feb 17 15:25:23 2025 +0300

    fix install check config

commit 17c4c26ea8
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 17:18:20 2025 +0300

    fix query log

commit 14a2685ae3
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 15:52:36 2025 +0300

    fix dhcp initial values

commit e7a8db7afd
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 14:37:24 2025 +0300

    fix encryption form values

commit 1c8917f7ac
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 14:07:29 2025 +0300

    fix blocked services submit

commit 4dfa536cea
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Feb 14 13:50:47 2025 +0300

    dns config ip validation

commit 4fee83fe13
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Feb 12 17:49:54 2025 +0300

    add playwright warning

commit 8c2f36e7a6
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 18:36:18 2025 +0300

    fix config file name

commit 83db5f33dc
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 16:16:43 2025 +0300

    temp config file

commit 9080c1620f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 15:01:46 2025 +0300

    update readme

commit ee1520307f
Merge: fd12e33c0 2fe2d254b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Feb 11 14:44:06 2025 +0300

    Merge branch 'master' into ADG-9415

commit fd12e33c06
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Feb 10 10:29:43 2025 +0100

    added typecheck on build, fixed eslint

commit b3849eebc4
Merge: 225167a8b 9bf3ee128
Author: Igor Lobanov <bniwredyc@gmail.com>
Date:   Mon Feb 10 09:43:32 2025 +0100

    Merge branch 'ADG-9415' of https://bit.int.agrd.dev/scm/dns/adguard-home into ADG-9415

... and 94 more commits
This commit is contained in:
Ildar Kamalov
2025-02-26 19:37:52 +03:00
parent 0389515ee3
commit 8b2ab8ea87
102 changed files with 7075 additions and 10256 deletions

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
import { ALL_INTERFACES_IP } from '../../helpers/constants';
import { DhcpInterface } from '../../initialState';
import { InstallInterface } from '../../initialState';
interface renderItemProps {
ip: string;
@@ -28,7 +28,7 @@ const renderItem = ({ ip, port, isDns }: renderItemProps) => {
};
interface AddressListProps {
interfaces: DhcpInterface[];
interfaces: InstallInterface[];
address: string;
port: number;
isDns?: boolean;

View File

@@ -1,47 +1,47 @@
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { withTranslation, Trans } from 'react-i18next';
import flow from 'lodash/flow';
import i18n from '../../i18n';
import { Controller, useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import Controls from './Controls';
import { validatePasswordLength, validateRequiredValue } from '../../helpers/validators';
import { Input } from '../../components/ui/Controls/Input';
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 <Trans>form_error_required</Trans>;
type AuthFormValues = {
username: string;
password: string;
confirm_password: string;
};
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;
type Props = {
onAuthSubmit: (values: AuthFormValues) => void;
};
interface AuthProps {
handleSubmit: (...args: unknown[]) => string;
pristine: boolean;
invalid: boolean;
t: (...args: unknown[]) => string;
}
export const Auth = ({ onAuthSubmit }: Props) => {
const { t } = useTranslation();
const {
handleSubmit,
watch,
control,
formState: { isDirty, isValid },
} = useForm<AuthFormValues>({
mode: 'onBlur',
defaultValues: {
username: '',
password: '',
confirm_password: '',
},
});
const Auth = (props: AuthProps) => {
const { handleSubmit, pristine, invalid, t } = props;
const password = watch('password');
const validateConfirmPassword = (value: string) => {
if (value !== password) {
return t('form_error_password');
}
return undefined;
};
return (
<form className="setup__step" onSubmit={handleSubmit}>
<form className="setup__step" onSubmit={handleSubmit(onAuthSubmit)}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_auth_title</Trans>
@@ -52,65 +52,74 @@ const Auth = (props: AuthProps) => {
</p>
<div className="form-group">
<label>
<Trans>install_auth_username</Trans>
</label>
<Field
<Controller
name="username"
component={renderInputField}
type="text"
className="form-control"
placeholder={t('install_auth_username_enter')}
validate={[required]}
autoComplete="username"
control={control}
rules={{ validate: validateRequiredValue }}
render={({ field, fieldState }) => (
<Input
{...field}
type="text"
data-testid="install_username"
label={t('install_auth_username')}
placeholder={t('install_auth_username_enter')}
error={fieldState.error?.message}
autoComplete="username"
/>
)}
/>
</div>
<div className="form-group">
<label>
<Trans>install_auth_password</Trans>
</label>
<Field
<Controller
name="password"
component={renderInputField}
type="password"
className="form-control"
placeholder={t('install_auth_password_enter')}
validate={[required, validatePasswordLength]}
autoComplete="new-password"
control={control}
rules={{
validate: {
required: validateRequiredValue,
passwordLength: validatePasswordLength,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="password"
data-testid="install_password"
label={t('install_auth_password')}
placeholder={t('install_auth_password_enter')}
error={fieldState.error?.message}
autoComplete="new-password"
/>
)}
/>
</div>
<div className="form-group">
<label>
<Trans>install_auth_confirm</Trans>
</label>
<Field
<Controller
name="confirm_password"
component={renderInputField}
type="password"
className="form-control"
placeholder={t('install_auth_confirm')}
validate={[required]}
autoComplete="new-password"
control={control}
rules={{
validate: {
required: validateRequiredValue,
confirmPassword: validateConfirmPassword,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="password"
data-testid="install_confirm_password"
label={t('install_auth_confirm')}
placeholder={t('install_auth_confirm')}
error={fieldState.error?.message}
autoComplete="new-password"
/>
)}
/>
</div>
</div>
<Controls pristine={pristine} invalid={invalid} />
<Controls isDirty={isDirty} isValid={isValid} />
</form>
);
};
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.INSTALL,
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
validate,
}),
])(Auth);

View File

@@ -32,6 +32,7 @@ class Controls extends Component<ControlsProps> {
case 3:
return (
<button
data-testid="install_back"
type="button"
className="btn btn-secondary btn-lg setup__button"
onClick={this.props.prevStep}>
@@ -44,24 +45,16 @@ class Controls extends Component<ControlsProps> {
}
renderNextButton(step: any) {
const {
nextStep,
invalid,
pristine,
install,
ip,
port,
} = this.props;
const { nextStep, invalid, pristine, install, ip, port } = this.props;
switch (step) {
case 1:
return (
<button type="button" className="btn btn-success btn-lg setup__button" onClick={nextStep}>
<button
data-testid="install_get_started"
type="button"
className="btn btn-success btn-lg setup__button"
onClick={nextStep}>
<Trans>get_started</Trans>
</button>
);
@@ -69,6 +62,7 @@ class Controls extends Component<ControlsProps> {
case 3:
return (
<button
data-testid="install_next"
type="submit"
className="btn btn-success btn-lg setup__button"
disabled={invalid || pristine || install.processingSubmit}>
@@ -77,13 +71,18 @@ class Controls extends Component<ControlsProps> {
);
case 4:
return (
<button type="button" className="btn btn-success btn-lg setup__button" onClick={nextStep}>
<button
data-testid="install_next"
type="button"
className="btn btn-success btn-lg setup__button"
onClick={nextStep}>
<Trans>next</Trans>
</button>
);
case 5:
return (
<button
data-testid="install_open_dashboard"
type="button"
className="btn btn-success btn-lg setup__button"
onClick={() => this.props.openDashboard(ip, port)}>

View File

@@ -1,25 +1,21 @@
import React from 'react';
import { connect } from 'react-redux';
import { reduxForm, formValueSelector } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { Trans } from 'react-i18next';
import Guide from '../../components/ui/Guide';
import { Guide } from '../../components/ui/Guide';
import Controls from './Controls';
import AddressList from './AddressList';
import { FORM_NAME } from '../../helpers/constants';
import { DhcpInterface } from '../../initialState';
import { InstallInterface } from '../../initialState';
import { DnsConfig } from './Settings';
interface DevicesProps {
interfaces: DhcpInterface[];
dnsIp: string;
dnsPort: number;
}
type Props = {
interfaces: InstallInterface[];
dnsConfig: DnsConfig;
};
let Devices = (props: DevicesProps) => (
export const Devices = ({ interfaces, dnsConfig }: Props) => (
<div className="setup__step">
<div className="setup__group">
<div className="setup__subtitle">
@@ -34,7 +30,7 @@ let Devices = (props: DevicesProps) => (
</div>
<div className="mt-1">
<AddressList interfaces={props.interfaces} address={props.dnsIp} port={props.dnsPort} isDns />
<AddressList interfaces={interfaces} address={dnsConfig.ip} port={dnsConfig.port} isDns />
</div>
</div>
@@ -44,24 +40,3 @@ let Devices = (props: DevicesProps) => (
<Controls />
</div>
);
const selector = formValueSelector('install');
Devices = connect((state) => {
const dnsIp = selector(state, 'dns.ip');
const dnsPort = selector(state, 'dns.port');
return {
dnsIp,
dnsPort,
};
})(Devices);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.INSTALL,
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
}),
])(Devices);

View File

@@ -1,21 +1,19 @@
import React from 'react';
import { Trans, withTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import { INSTALL_TOTAL_STEPS } from '../../helpers/constants';
const getProgressPercent = (step: any) => (step / INSTALL_TOTAL_STEPS) * 100;
const getProgressPercent = (step: number) => (step / INSTALL_TOTAL_STEPS) * 100;
type Props = {
step: number;
};
const Progress = (props: Props) => (
export const Progress = ({ step }: Props) => (
<div className="setup__progress">
<Trans>install_step</Trans> {props.step}/{INSTALL_TOTAL_STEPS}
<Trans>install_step</Trans> {step}/{INSTALL_TOTAL_STEPS}
<div className="setup__progress-wrap">
<div className="setup__progress-inner" style={{ width: `${getProgressPercent(props.step)}%` }} />
<div className="setup__progress-inner" style={{ width: `${getProgressPercent(step)}%` }} />
</div>
</div>
);
export default withTranslation()(Progress);

View File

@@ -1,32 +1,84 @@
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 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,
MIN_PORT,
} from '../../helpers/constants';
import { renderInputField, toNumber } from '../../helpers/form';
import { validateRequiredValue, validateInstallPort } from '../../helpers/validators';
import { DhcpInterface } from '../../initialState';
import { validateRequiredValue } from '../../helpers/validators';
import { InstallInterface } from '../../initialState';
import { Input } from '../../components/ui/Controls/Input';
import { Select } from '../../components/ui/Controls/Select';
import { toNumber } from '../../helpers/form';
const renderInterfaces = (interfaces: DhcpInterface[]) =>
Object.values(interfaces).map((option: DhcpInterface) => {
const validateInstallPort = (value: number) => {
if (value < MIN_PORT || value > MAX_PORT) {
return i18n.t('form_error_port');
}
return undefined;
};
export type WebConfig = {
ip: string;
port: number;
};
export type DnsConfig = {
ip: string;
port: number;
};
export type SettingsFormValues = {
web: WebConfig;
dns: DnsConfig;
};
type StaticIpType = {
ip: string;
static: string;
};
export 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: SettingsFormValues) => void;
handleChange?: (data: SettingsFormValues) => unknown;
handleFix: (web: WebConfig, dns: DnsConfig, set_static_ip: boolean) => void;
validateForm: (data: SettingsFormValues) => void;
config: ConfigType;
interfaces: InstallInterface[];
initialValues?: object;
};
const renderInterfaces = (interfaces: InstallInterface[]) =>
Object.values(interfaces).map((option: InstallInterface) => {
const { name, ip_addresses, flags } = option;
if (option && ip_addresses?.length > 0) {
@@ -43,113 +95,70 @@ 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: {
export const Settings = ({ handleSubmit, handleFix, validateForm, config, interfaces }: Props) => {
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<Props> {
componentDidMount() {
const { webIp, webPort, dnsIp, dnsPort } = this.props;
const {
control,
watch,
handleSubmit: reactHookFormSubmit,
formState: { isValid },
} = useForm<SettingsFormValues>({
defaultValues,
mode: 'onBlur',
});
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(() => {
const webPortError = validateInstallPort(webPortVal);
const dnsPortError = validateInstallPort(dnsPortVal);
if (webPortError || dnsPortError) {
return;
}
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 (
<>
<div className="mb-2">
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
install_static_configure
</Trans>
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={() => this.handleStaticIp(ip)}>
<Trans>set_static_ip</Trans>
</button>
</>
);
}
case STATUS_RESPONSE.ERROR: {
return (
<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;
}
};
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 +172,292 @@ class Settings extends Component<Props> {
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 (
<>
<div className="mb-2">
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
install_static_configure
</Trans>
</div>
webIp,
<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>
);
case STATUS_RESPONSE.YES:
return (
<div className="text-success">
<Trans>install_static_ok</Trans>
</div>
);
default:
return null;
}
},
[handleStaticIp],
);
webPort,
const onSubmit = (data: SettingsFormValues) => {
validateForm(data);
handleSubmit(data);
};
dnsIp,
return (
<form className="setup__step" onSubmit={reactHookFormSubmit(onSubmit)}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_title</Trans>
</div>
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 (
<form className="setup__step" onSubmit={handleSubmit}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_title</Trans>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Controller
name="web.ip"
control={control}
render={({ field }) => (
<Select {...field} data-testid="install_web_ip">
<option value={ALL_INTERFACES_IP}>
{t('install_settings_all_interfaces')}
</option>
{renderInterfaces(interfaces)}
</Select>
)}
/>
</div>
</div>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Field
name="web.ip"
component="select"
className="form-control custom-select"
onChange={handleChange}>
<option value={ALL_INTERFACES_IP}>
{this.props.t('install_settings_all_interfaces')}
</option>
{renderInterfaces(interfaces)}
</Field>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Controller
name="web.port"
control={control}
rules={{
validate: {
required: validateRequiredValue,
installPort: validateInstallPort,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="number"
data-testid="install_web_port"
placeholder={STANDARD_WEB_PORT.toString()}
error={fieldState.error?.message}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="web.port"
component={renderInputField}
type="number"
className="form-control"
placeholder={STANDARD_WEB_PORT.toString()}
validate={[validateInstallPort, validateRequiredValue]}
normalize={toNumber}
onChange={handleChange}
/>
<div className="col-12">
{webStatus && (
<div className="setup__error text-danger">
{webStatus}
{isWebFixAvailable && (
<button
type="button"
data-testid="install_web_fix"
className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('web')}>
<Trans>fix</Trans>
</button>
)}
</div>
</div>
)}
<div className="col-12">
{webStatus && (
<hr className="divider--small" />
</div>
</div>
<div className="setup__desc">
<Trans>install_settings_interface_link</Trans>
<div className="mt-1">
<AddressList
interfaces={interfaces}
address={watchFields.web?.ip}
port={watchFields.web?.port}
/>
</div>
</div>
</div>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_dns</Trans>
</div>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Controller
name="dns.ip"
control={control}
render={({ field }) => (
<Select {...field} data-testid="install_dns_ip">
<option value={ALL_INTERFACES_IP}>
{t('install_settings_all_interfaces')}
</option>
{renderInterfaces(interfaces)}
</Select>
)}
/>
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Controller
name="dns.port"
control={control}
rules={{
required: t('form_error_required'),
validate: {
required: validateRequiredValue,
installPort: validateInstallPort,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
type="number"
data-testid="install_dns_port"
error={fieldState.error?.message}
placeholder={STANDARD_WEB_PORT.toString()}
onChange={(e) => {
const { value } = e.target;
field.onChange(toNumber(value));
}}
/>
)}
/>
</div>
</div>
<div className="col-12">
{dnsStatus && (
<>
<div className="setup__error text-danger">
{webStatus}
{isWebFixAvailable && (
{dnsStatus}
{isDnsFixAvailable && (
<button
type="button"
data-testid="install_dns_fix"
className="btn btn-secondary btn-sm ml-2"
onClick={() => this.handleAutofix('web')}>
onClick={() => handleAutofix('dns')}>
<Trans>fix</Trans>
</button>
)}
</div>
)}
<hr className="divider--small" />
</div>
</div>
<div className="setup__desc">
<Trans>install_settings_interface_link</Trans>
<div className="mt-1">
<AddressList interfaces={interfaces} address={webIp} port={webPort} />
</div>
</div>
</div>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_dns</Trans>
</div>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Field
name="dns.ip"
component="select"
className="form-control custom-select"
onChange={handleChange}>
<option value={ALL_INTERFACES_IP}>{t('install_settings_all_interfaces')}</option>
{renderInterfaces(interfaces)}
</Field>
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="dns.port"
component={renderInputField}
type="number"
className="form-control"
placeholder={STANDARD_WEB_PORT.toString()}
validate={[validateInstallPort, validateRequiredValue]}
normalize={toNumber}
onChange={handleChange}
/>
</div>
</div>
<div className="col-12">
{dnsStatus && (
<>
<div className="setup__error text-danger">
{dnsStatus}
{isDnsFixAvailable && (
<button
type="button"
className="btn btn-secondary btn-sm ml-2"
onClick={() => this.handleAutofix('dns')}>
<Trans>fix</Trans>
</button>
)}
{isDnsFixAvailable && (
<div className="text-muted mb-2">
<p className="mb-1">
<Trans>autofix_warning_text</Trans>
</p>
<Trans components={[<li key="0">text</li>]}>autofix_warning_list</Trans>
<p className="mb-1">
<Trans>autofix_warning_result</Trans>
</p>
</div>
{isDnsFixAvailable && (
<div className="text-muted mb-2">
<p className="mb-1">
<Trans>autofix_warning_text</Trans>
</p>
<Trans components={[<li key="0">text</li>]}>autofix_warning_list</Trans>
<p className="mb-1">
<Trans>autofix_warning_result</Trans>
</p>
</div>
)}
</>
)}
{dnsPort === STANDARD_DNS_PORT &&
!isDnsFixAvailable &&
dnsStatus.includes(ADDRESS_IN_USE_TEXT) && (
<Trans
components={[
<a
href={PORT_53_FAQ_LINK}
key="0"
target="_blank"
rel="noopener noreferrer">
link
</a>,
]}>
port_53_faq_link
</Trans>
)}
</>
)}
{watchFields.dns?.port === STANDARD_DNS_PORT &&
!isDnsFixAvailable &&
dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && (
<Trans
components={[
<a href={PORT_53_FAQ_LINK} key="0" target="_blank" rel="noopener noreferrer">
link
</a>,
]}>
port_53_faq_link
</Trans>
)}
<hr className="divider--small" />
</div>
</div>
<div className="setup__desc">
<Trans>install_settings_dns_desc</Trans>
<div className="mt-1">
<AddressList interfaces={interfaces} address={dnsIp} port={dnsPort} isDns={true} />
</div>
<hr className="divider--small" />
</div>
</div>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>static_ip</Trans>
</div>
<div className="setup__desc">
<Trans>install_settings_dns_desc</Trans>
<div className="mb-2">
<Trans>static_ip_desc</Trans>
<div className="mt-1">
<AddressList
interfaces={interfaces}
address={watchFields.dns?.ip}
port={watchFields.dns?.port}
isDns={true}
/>
</div>
</div>
</div>
{this.getStaticIpMessage(staticIp)}
<div className="setup__group">
<div className="setup__subtitle">
<Trans>static_ip</Trans>
</div>
<Controls invalid={invalid} />
</form>
);
}
}
<div className="mb-2">
<Trans>static_ip_desc</Trans>
</div>
const selector = formValueSelector(FORM_NAME.INSTALL);
{getStaticIpMessage(staticIp)}
</div>
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);
<Controls invalid={!isValid} />
</form>
);
};

View File

@@ -1,23 +1,16 @@
import React from 'react';
import { connect } from 'react-redux';
import { reduxForm, formValueSelector } from 'redux-form';
import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow';
import { Trans } from 'react-i18next';
import Controls from './Controls';
import { FORM_NAME } from '../../helpers/constants';
import { WebConfig } from './Settings';
interface SubmitProps {
webIp: string;
webPort: number;
handleSubmit: (...args: unknown[]) => string;
pristine: boolean;
submitting: boolean;
openDashboard: (...args: unknown[]) => unknown;
}
type Props = {
webConfig: WebConfig;
openDashboard: (ip: string, port: number) => void;
};
let Submit = (props: SubmitProps) => (
export const Submit = ({ openDashboard, webConfig }: Props) => (
<div className="setup__step">
<div className="setup__group">
<h1 className="setup__title">
@@ -29,27 +22,6 @@ let Submit = (props: SubmitProps) => (
</p>
</div>
<Controls openDashboard={props.openDashboard} ip={props.webIp} port={props.webPort} />
<Controls openDashboard={openDashboard} ip={webConfig.ip} port={webConfig.port} />
</div>
);
const selector = formValueSelector('install');
Submit = connect((state) => {
const webIp = selector(state, 'web.ip');
const webPort = selector(state, 'web.port');
return {
webIp,
webPort,
};
})(Submit);
export default flow([
withTranslation(),
reduxForm({
form: FORM_NAME.INSTALL,
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
}),
])(Submit);

View File

@@ -1,101 +1,80 @@
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 { ConfigType, DnsConfig, Settings, WebConfig } from './Settings';
import { Devices } from './Devices';
import { Submit } from './Submit';
import { Progress } from './Progress';
import { Auth } from './Auth';
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 { InstallInterface, InstallState } from '../../initialState';
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;
}
export const Setup = () => {
const dispatch = useDispatch();
class Setup extends Component<SetupProps> {
componentDidMount() {
this.props.getDefaultAddresses();
}
const install = useSelector((state: InstallState) => 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: WebConfig, dns: DnsConfig, set_static_ip: boolean) => {
dispatch(actionCreators.checkConfig({ web, dns, set_static_ip }));
};
openDashboard = (ip: any, port: any) => {
const openDashboard = (ip: string, port: number) => {
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: number, config: ConfigType, interfaces: InstallInterface[]) => {
switch (step) {
case 1:
return <Greeting />;
@@ -105,55 +84,41 @@ class Setup extends Component<SetupProps> {
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 <Auth onSubmit={this.handleFormSubmit} />;
return <Auth onAuthSubmit={handleFormSubmit} />;
case 4:
return <Devices interfaces={interfaces} />;
return <Devices interfaces={interfaces} dnsConfig={dns} />;
case 5:
return <Submit openDashboard={this.openDashboard} />;
return <Submit openDashboard={openDashboard} webConfig={web} />;
default:
return false;
}
};
if (processingDefault) {
return <Loading />;
}
render() {
const { processingDefault, step, web, dns, staticIp, interfaces } = this.props.install;
return (
<>
<div className="setup">
<div className="setup__container">
<Logo className="setup__logo" />
{renderPage(step, { web, dns, staticIp }, interfaces)}
<Progress step={step} />
</div>
</div>
return (
<Fragment>
{processingDefault && <Loading />}
{!processingDefault && (
<Fragment>
<div className="setup">
<div className="setup__container">
<Logo className="setup__logo" />
{this.renderPage(step, { web, dns, staticIp }, interfaces)}
<Progress step={step} />
</div>
</div>
<Footer />
<Footer />
<Toasts />
<Toasts />
<Icons />
</Fragment>
)}
</Fragment>
);
}
}
const mapStateToProps = (state: any) => {
const { install, toasts } = state;
const props = { install, toasts };
return props;
<Icons />
</>
);
};
export default connect(mapStateToProps, actionCreators)(Setup);

View File

@@ -8,7 +8,7 @@ import configureStore from '../configureStore';
import reducers from '../reducers/install';
import '../i18n';
import Setup from './Setup';
import { Setup } from './Setup';
import { InstallState } from '../initialState';
const store = configureStore<InstallState>(reducers, {});