Pull request: beta client squashed

Merge in DNS/adguard-home from beta-client-2 to master

Squashed commit of the following:

commit b2640cc49a6c5484d730b534dcf5a8013d7fa478
Merge: 659def862 aef4659e9
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Dec 29 19:23:09 2020 +0300

    Merge branch 'master' into beta-client-2

commit 659def8626467949c35b7a6a0c99ffafb07b4385
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Dec 29 17:25:14 2020 +0300

    all: upgrade github actions node version

commit b4b8cf8dd75672e9155da5d111ac66e8f5ba1535
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Tue Dec 29 16:57:14 2020 +0300

    all: beta client squashed
This commit is contained in:
Eugene Burkov
2020-12-29 19:53:56 +03:00
parent aef4659e93
commit 5e20ac7ed5
200 changed files with 20843 additions and 55 deletions

View File

@@ -0,0 +1,14 @@
.layout {
background-image: url('../../assets/img/background_min.png');
background-repeat: no-repeat;
min-height: 100vh;
background-color: #f1f3f7;
}
.container {
display: flex;
justify-content: center;
padding: 40px;
}
.content {
max-width: 404px;
}

View File

@@ -0,0 +1,122 @@
import React, { FC } from 'react';
import { Layout } from 'antd';
import { Formik, FormikHelpers } from 'formik';
import { observer } from 'mobx-react-lite';
import { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta';
import Icons from 'Lib/theme/Icons';
import {
DEFAULT_DNS_ADDRESS,
DEFAULT_DNS_PORT,
DEFAULT_IP_ADDRESS,
DEFAULT_IP_PORT,
} from 'Consts/install';
import { notifyError } from 'Common/ui';
import InstallStore from 'Store/stores/Install';
import AdminInterface from './components/AdminInterface';
import Auth from './components/Auth';
import DnsServer from './components/DnsServer';
import Stepper from './components/Stepper';
import Welcome from './components/Welcome';
import ConfigureDevices from './components/ConfigureDevices';
import s from './Install.module.pcss';
const { Content } = Layout;
export type FormValues = IInitialConfigurationBeta & { step: number };
const InstallForm: FC = observer(() => {
const initialValues: FormValues = {
step: 0,
web: {
ip: [DEFAULT_IP_ADDRESS],
port: DEFAULT_IP_PORT,
},
dns: {
ip: [DEFAULT_DNS_ADDRESS],
port: DEFAULT_DNS_PORT,
},
password: '',
username: '',
};
const onNext = async (values: FormValues, { setFieldValue }: FormikHelpers<FormValues>) => {
const currentStep = values.step;
const checker = (condition: boolean, message: string) => {
if (condition) {
setFieldValue('step', currentStep + 1);
} else {
notifyError(message);
}
};
switch (currentStep) {
case 1: {
// web
const check = await InstallStore.checkConfig(values);
checker(check?.web?.status === '', check?.web?.status || '');
break;
}
case 3: {
// dns
const check = await InstallStore.checkConfig(values);
checker(check?.dns?.status === '', check?.dns?.status || '');
break;
}
case 4: {
// configure
const config = await InstallStore.configure(values);
if (config) {
const { web } = values;
window.location.href = `http://${web.ip[0]}:${web.port}`;
}
break;
}
default:
setFieldValue('step', currentStep + 1);
break;
}
};
return (
<Formik
initialValues={initialValues}
onSubmit={onNext}
>
{({ values, handleSubmit, setFieldValue }) => (
<form noValidate className={s.content} onSubmit={handleSubmit}>
<Stepper currentStep={values.step} />
{values.step === 0 && (
<Welcome onNext={() => setFieldValue('step', 1)}/>
)}
{values.step === 1 && (
<AdminInterface values={values} setFieldValue={setFieldValue} />
)}
{values.step === 2 && (
<Auth values={values} setFieldValue={setFieldValue} />
)}
{values.step === 3 && (
<DnsServer values={values} setFieldValue={setFieldValue} />
)}
{values.step === 4 && (
<ConfigureDevices values={values} setFieldValue={setFieldValue} />
)}
</form>
)}
</Formik>
);
});
const Install: FC = () => {
return (
<Layout className={s.layout}>
<Content className={s.container}>
<InstallForm />
</Content>
<Icons/>
</Layout>
);
};
export default Install;

View File

@@ -0,0 +1,17 @@
.manualOptions {
margin-bottom: 48px;
}
.name {
padding-bottom: 5px;
border-bottom: 1px solid var(--gray300);
margin-bottom: 16px;
margin-top: 20px;
}
.manualOption {
display: flex;
justify-content: space-between;
align-items: baseline;
}

View File

@@ -0,0 +1,145 @@
import React, { FC, useContext } from 'react';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import { FormikHelpers } from 'formik';
import { Input, Radio, Switch } from 'Common/controls';
import { DEFAULT_IP_ADDRESS } from 'Consts/install';
import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers';
import theme from 'Lib/theme';
import Store from 'Store/installStore';
import s from './AdminInterface.module.pcss';
import { FormValues } from '../../Install';
import StepButtons from '../StepButtons';
enum NETWORK_OPTIONS {
ALL = 'all',
CUSTOM = 'custom',
}
interface AdminInterfaceProps {
values: FormValues;
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
}
const AdminInterface: FC<AdminInterfaceProps> = observer(({
values,
setFieldValue,
}) => {
const { ui: { intl }, install: { addresses } } = useContext(Store);
const { web: { ip } } = values;
const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS
? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM;
const onSelectRadio = (v: string | number) => {
const value = v === NETWORK_OPTIONS.ALL
? [DEFAULT_IP_ADDRESS] : [];
setFieldValue('web.ip', value);
};
const getManualBlock = () => (
<div className={s.manualOptions}>
{addresses?.interfaces.map((a) => {
let name = '';
const type = chechNetworkType(a.name);
switch (type) {
case NETWORK_TYPE.ETHERNET:
name = `${intl.getMessage('ethernet')} (${a.name}) `;
break;
case NETWORK_TYPE.LOCAL:
name = `${intl.getMessage('localhost')} (${a.name}) `;
break;
default:
name = a.name || '';
break;
}
return (
<div key={a.name}>
<div>
<div className={s.name}>
{name}
</div>
{a.ipAddresses?.map((addrIp) => (
<div key={addrIp} className={s.manualOption}>
<div className={theme.typography.subtext}>
http://{addrIp}
</div>
<Switch
checked={values.web.ip.includes(addrIp)}
onChange={() => {
const temp = new Set(ip);
if (temp.has(addrIp)) {
temp.delete(addrIp);
} else {
temp.add(addrIp);
}
setFieldValue('web.ip', Array.from(temp.values()));
}}/>
</div>
))}
</div>
</div>
);
})}
</div>
);
return (
<div className={s.content}>
<div className={theme.typography.title}>
{intl.getMessage('install_admin_interface_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
{intl.getMessage('install_admin_interface_title_decs')}
</div>
<div className={theme.typography.subTitle}>
{intl.getMessage('install_admin_interface_where_interface')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_admin_interface_where_interface_desc')}
</div>
<Radio
value={radioValue}
onSelect={onSelectRadio}
options={[
{
value: NETWORK_OPTIONS.ALL,
label: intl.getMessage('install_all_networks'),
desc: intl.getMessage('install_all_networks_description'),
},
{
value: NETWORK_OPTIONS.CUSTOM,
label: intl.getMessage('install_choose_networks'),
desc: intl.getMessage('install_choose_networks_desc'),
},
]}
/>
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
<div className={theme.typography.subTitle}>
{intl.getMessage('install_admin_interface_port')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_admin_interface_port_desc')}
</div>
<Input
label={`${intl.getMessage('port')}:`}
placeholder={intl.getMessage('port')}
type="number"
name="webPort"
value={values.web.port}
onChange={(v) => {
const port = v === '' ? '' : parseInt(v, 10);
setFieldValue('web.port', port);
}}
/>
<StepButtons
setFieldValue={setFieldValue}
currentStep={1}
values={values}
/>
</div>
);
});
export default AdminInterface;

View File

@@ -0,0 +1 @@
export { default } from './AdminInterface';

View File

@@ -0,0 +1,55 @@
import React, { FC, useContext } from 'react';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import { FormikHelpers } from 'formik';
import { Input } from 'Common/controls';
import theme from 'Lib/theme';
import Store from 'Store/installStore';
import StepButtons from '../StepButtons';
import { FormValues } from '../../Install';
interface AuthProps {
values: FormValues;
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
}
const Auth: FC<AuthProps> = observer(({
values,
setFieldValue,
}) => {
const { ui: { intl } } = useContext(Store);
return (
<div>
<div className={theme.typography.title}>
{intl.getMessage('install_auth_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
{intl.getMessage('install_auth_description')}
</div>
<Input
placeholder={intl.getMessage('login')}
type="username"
name="username"
value={values.username}
onChange={(v) => setFieldValue('username', v)}
/>
<Input
placeholder={intl.getMessage('password')}
type="password"
name="password"
value={values.password}
onChange={(v) => setFieldValue('password', v)}
/>
<StepButtons
setFieldValue={setFieldValue}
currentStep={2}
values={values}
/>
</div>
);
});
export default Auth;

View File

@@ -0,0 +1 @@
export { default } from './Auth';

View File

@@ -0,0 +1,4 @@
.tabs {
width: 505px;
margin-left: -131px;
}

View File

@@ -0,0 +1,128 @@
import React, { FC, useContext } from 'react';
import { Tabs } from 'antd';
import cn from 'classnames';
import { FormikHelpers } from 'formik';
import Store from 'Store/installStore';
import theme from 'Lib/theme';
import { danger, p } from 'Common/formating';
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
import { FormValues } from '../../Install';
import StepButtons from '../StepButtons';
import s from './ConfigureDevices.module.pcss';
const { TabPane } = Tabs;
interface ConfigureDevicesProps {
values: FormValues;
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
}
const ConfigureDevices: FC<ConfigureDevicesProps> = ({
values, setFieldValue,
}) => {
const { ui: { intl }, install: { addresses } } = useContext(Store);
const dhcp = (e: string) => (
// TODO: link to dhcp
<a href="http://" target="_blank" rel="noopener noreferrer">{e}</a>
);
const allIps = addresses?.interfaces.reduce<string[]>((all, data) => {
const { ipAddresses } = data;
if (ipAddresses) {
all.push(...ipAddresses);
}
return all;
}, [] as string[]);
const { web: { ip: webIp }, dns: { ip: dnsIp } } = values;
const selectedWebIps = webIp.length === 1 && webIp[0] === DEFAULT_IP_ADDRESS
? allIps : webIp;
const selectedDnsIps = dnsIp.length === 1 && dnsIp[0] === DEFAULT_IP_ADDRESS
? allIps : dnsIp;
return (
<div>
<div className={theme.typography.title}>
{intl.getMessage('install_configure_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
{intl.getMessage('install_configure_danger_notice', { danger })}
</div>
<div className={theme.typography.subTitle}>
{intl.getMessage('install_configure_how_to_title')}
</div>
<Tabs defaultActiveKey="1" tabPosition="left" className={s.tabs}>
<TabPane tab="Router" key="1">
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_configure_router', { p })}
</div>
</TabPane>
<TabPane tab="Windows" key="2">
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_configure_windows', { p })}
</div>
</TabPane>
<TabPane tab="Macos" key="3">
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_configure_macos', { p })}
</div>
</TabPane>
<TabPane tab="Linux" key="4">
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{/* TODO: add linux setup */}
{intl.getMessage('install_configure_router', { p })}
</div>
</TabPane>
<TabPane tab="Android" key="5">
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_configure_android', { p })}
</div>
</TabPane>
<TabPane tab="iOs" key="6">
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_configure_ios', { p })}
</div>
</TabPane>
</Tabs>
<div className={theme.typography.subTitle}>
{intl.getMessage('install_configure_adresses')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
<p>
{intl.getMessage('install_admin_interface_title')}
</p>
<p>
{selectedWebIps?.map((ip) => (
<div key={ip}>
{ip}{values.web.port !== DEFAULT_IP_PORT && `:${values.web.port}`}
</div>
))}
</p>
<p>
{intl.getMessage('install_dns_server_title')}
</p>
<div>
{selectedDnsIps?.map((ip) => (
<div key={ip}>
{ip}{values.dns.port !== DEFAULT_DNS_PORT && `:${values.dns.port}`}
</div>
))}
</div>
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_configure_dhcp', { dhcp })}
</div>
<StepButtons
setFieldValue={setFieldValue}
currentStep={4}
values={values}
/>
</div>
);
};
export default ConfigureDevices;

View File

@@ -0,0 +1 @@
export { default } from './ConfigureDevices';

View File

@@ -0,0 +1,12 @@
.manualOptions {
margin-bottom: 48px;
}
.manualOption {
display: flex;
justify-content: space-between;
align-items: baseline;
padding-bottom: 16px;
border-bottom: 1px solid var(--gray300);
margin-bottom: 16px;
}

View File

@@ -0,0 +1,145 @@
import React, { FC, useContext } from 'react';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import { FormikHelpers } from 'formik';
import { Input, Radio, Switch } from 'Common/controls';
import { DEFAULT_IP_ADDRESS } from 'Consts/install';
import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers';
import theme from 'Lib/theme';
import Store from 'Store/installStore';
import s from './DnsServer.module.pcss';
import { FormValues } from '../../Install';
import StepButtons from '../StepButtons';
enum NETWORK_OPTIONS {
ALL = 'all',
CUSTOM = 'custom',
}
interface DnsServerProps {
values: FormValues;
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
}
const DnsServer: FC<DnsServerProps> = observer(({
values,
setFieldValue,
}) => {
const { ui: { intl }, install: { addresses } } = useContext(Store);
const { dns: { ip } } = values;
const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS
? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM;
const onSelectRadio = (v: string | number) => {
const value = v === NETWORK_OPTIONS.ALL
? [DEFAULT_IP_ADDRESS] : [];
setFieldValue('dns.ip', value);
};
const getManualBlock = () => (
<div className={s.manualOptions}>
{addresses?.interfaces.map((a) => {
let name = '';
const type = chechNetworkType(a.name);
switch (type) {
case NETWORK_TYPE.ETHERNET:
name = `${intl.getMessage('ethernet')} (${a.name}) `;
break;
case NETWORK_TYPE.LOCAL:
name = `${intl.getMessage('localhost')} (${a.name}) `;
break;
default:
name = a.name || '';
break;
}
return (
<div key={a.name}>
<div>
<div className={s.name}>
{name}
</div>
{a.ipAddresses?.map((addrIp) => (
<div key={addrIp} className={s.manualOption}>
<div className={theme.typography.subtext}>
{addrIp}
</div>
<Switch
checked={values.dns.ip.includes(addrIp)}
onChange={() => {
const temp = new Set(ip);
if (temp.has(addrIp)) {
temp.delete(addrIp);
} else {
temp.add(addrIp);
}
setFieldValue('dns.ip', Array.from(temp.values()));
}}/>
</div>
))}
</div>
</div>
);
})}
</div>
);
return (
<div>
<div className={theme.typography.title}>
{intl.getMessage('install_dns_server_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
{intl.getMessage('install_dns_server_desc')}
</div>
<div className={theme.typography.subTitle}>
{intl.getMessage('install_dns_server_network_interfaces')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_dns_server_network_interfaces_desc')}
</div>
<Radio
value={radioValue}
onSelect={onSelectRadio}
options={[
{
value: NETWORK_OPTIONS.ALL,
label: intl.getMessage('install_all_networks'),
desc: intl.getMessage('install_all_networks_description'),
},
{
value: NETWORK_OPTIONS.CUSTOM,
label: intl.getMessage('install_choose_networks'),
desc: intl.getMessage('install_choose_networks_desc'),
},
]}
/>
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
<div className={theme.typography.subTitle}>
{intl.getMessage('install_dns_server_port')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
{intl.getMessage('install_dns_server_port_desc')}
</div>
<Input
label={`${intl.getMessage('port')}:`}
placeholder={intl.getMessage('port')}
type="number"
name="dnsPort"
value={values.dns.port}
onChange={(v) => {
const port = v === '' ? '' : parseInt(v, 10);
setFieldValue('dns.port', port);
}}
/>
<StepButtons
setFieldValue={setFieldValue}
currentStep={3}
values={values}
/>
</div>
);
});
export default DnsServer;

View File

@@ -0,0 +1 @@
export { default } from './DnsServer';

View File

@@ -0,0 +1,8 @@
.button {
margin-top: 48px;
width: 190px;
&.inGroup {
margin-right: 24px;
}
}

View File

@@ -0,0 +1,45 @@
import React, { FC, useContext } from 'react';
import { Button } from 'antd';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import { FormikHelpers } from 'formik';
import Store from 'Store/installStore';
import { FormValues } from '../../Install';
import s from './StepButtons.module.pcss';
interface StepButtonsProps {
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
currentStep: number;
values: FormValues;
}
const StepButtons: FC<StepButtonsProps> = observer(({
setFieldValue,
currentStep,
}) => {
const { ui: { intl } } = useContext(Store);
return (
<div>
<Button
size="large"
type="ghost"
className={cn(s.button, s.inGroup)}
onClick={() => setFieldValue('step', currentStep - 1)}
>
{intl.getMessage('back')}
</Button>
<Button
size="large"
type="primary"
htmlType="submit"
className={cn(s.button)}
>
{intl.getMessage('next')}
</Button>
</div>
);
});
export default StepButtons;

View File

@@ -0,0 +1 @@
export { default } from './StepButtons';

View File

@@ -0,0 +1,3 @@
.stepper {
margin-bottom: 48px;
}

View File

@@ -0,0 +1,24 @@
import React, { FC } from 'react';
import { Steps } from 'antd';
import s from './Stepper.module.pcss';
interface StepperProps {
currentStep: number;
}
const { Step } = Steps;
const Stepper: FC<StepperProps> = ({ currentStep }) => {
return (
<Steps progressDot current={currentStep} className={s.stepper}>
<Step/>
<Step/>
<Step/>
<Step/>
<Step/>
</Steps>
);
};
export default Stepper;

View File

@@ -0,0 +1 @@
export { default } from './Stepper';

View File

@@ -0,0 +1,15 @@
.iconContainer{
margin-bottom: 48px;
}
.icon {
width: 185px;
height: 57px;
}
.button {
margin-top: 48px;
width: 190px;
&.inGroup {
margin-right: 24px;
}
}

View File

@@ -0,0 +1,40 @@
import React, { FC, useContext } from 'react';
import { Button } from 'antd';
import { observer } from 'mobx-react-lite';
import Store from 'Store/installStore';
import Icon from 'Common/ui/Icon';
import theme from 'Lib/theme';
import s from './Welcome.module.pcss';
interface WelcomeProps {
onNext: () => void;
}
const Welcome: FC<WelcomeProps> = observer(({ onNext }) => {
const { ui: { intl } } = useContext(Store);
return (
<div className={s.content}>
<div className={s.iconContainer}>
<Icon icon="mainLogo" className={s.icon} />
</div>
<div className={theme.typography.title}>
{intl.getMessage('install_wellcome_title')}
</div>
<div className={theme.typography.text}>
{intl.getMessage('install_wellcome_desc')}
</div>
<Button
size="large"
type="primary"
className={s.button}
onClick={onNext}
>
{intl.getMessage('install_wellcome_button')}
</Button>
</div>
);
});
export default Welcome;

View File

@@ -0,0 +1 @@
export { default } from './Welcome';

View File

@@ -0,0 +1 @@
export { default } from './Install';