Merge: client: fix mobile layout for install page

Squashed commit of the following:

commit 5e620f2d8576b08ebfee08e9781cd4927c4dcf2a
Merge: d82d5a902 679bbcdc2
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Jan 18 14:57:00 2021 +0300

    Merge branch 'master' into 2554-mobile-install

commit d82d5a9028be0be72e612fc4c375d2be81c6c8c3
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Jan 18 14:09:25 2021 +0300

    client: fix mobile layout for install page
This commit is contained in:
Ildar Kamalov
2021-01-18 15:09:04 +03:00
parent 679bbcdc26
commit 9f75725dfa
31 changed files with 462 additions and 375 deletions

View File

@@ -1,14 +0,0 @@
.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

@@ -13,6 +13,7 @@ import {
} from 'Consts/install';
import { notifyError } from 'Common/ui';
import InstallStore from 'Store/stores/Install';
import theme from 'Lib/theme';
import AdminInterface from './components/AdminInterface';
import Auth from './components/Auth';
@@ -21,8 +22,6 @@ 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 };
@@ -85,7 +84,7 @@ const InstallForm: FC = observer(() => {
onSubmit={onNext}
>
{({ values, handleSubmit, setFieldValue }) => (
<form noValidate className={s.content} onSubmit={handleSubmit}>
<form noValidate onSubmit={handleSubmit}>
<Stepper currentStep={values.step} />
{values.step === 0 && (
<Welcome onNext={() => setFieldValue('step', 1)}/>
@@ -110,8 +109,8 @@ const InstallForm: FC = observer(() => {
const Install: FC = () => {
return (
<Layout className={s.layout}>
<Content className={s.container}>
<Layout className={theme.install.layout}>
<Content className={theme.install.container}>
<InstallForm />
</Content>
<Icons/>

View File

@@ -1,17 +0,0 @@
.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

@@ -9,7 +9,6 @@ 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';
@@ -39,7 +38,7 @@ const AdminInterface: FC<AdminInterfaceProps> = observer(({
};
const getManualBlock = () => (
<div className={s.manualOptions}>
<div className={theme.install.options}>
{addresses?.interfaces.map((a) => {
let name = '';
const type = chechNetworkType(a.name);
@@ -56,29 +55,27 @@ const AdminInterface: FC<AdminInterfaceProps> = observer(({
}
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 className={theme.install.name}>
{name}
</div>
{a.ipAddresses?.map((addrIp) => (
<div key={addrIp} className={theme.install.option}>
<div className={theme.install.address}>
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>
);
})}
@@ -86,17 +83,17 @@ const AdminInterface: FC<AdminInterfaceProps> = observer(({
);
return (
<div className={s.content}>
<div className={theme.typography.title}>
<>
<div className={theme.install.title}>
{intl.getMessage('install_admin_interface_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
<div className={cn(theme.install.text, theme.install.text_block)}>
{intl.getMessage('install_admin_interface_title_decs')}
</div>
<div className={theme.typography.subTitle}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_admin_interface_where_interface')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_admin_interface_where_interface_desc')}
</div>
<Radio
@@ -116,10 +113,10 @@ const AdminInterface: FC<AdminInterfaceProps> = observer(({
]}
/>
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
<div className={theme.typography.subTitle}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_admin_interface_port')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_admin_interface_port_desc')}
</div>
<Input
@@ -138,7 +135,7 @@ const AdminInterface: FC<AdminInterfaceProps> = observer(({
currentStep={1}
values={values}
/>
</div>
</>
);
});

View File

@@ -22,11 +22,11 @@ const Auth: FC<AuthProps> = observer(({
const { ui: { intl } } = useContext(Store);
return (
<div>
<div className={theme.typography.title}>
<>
<div className={theme.install.title}>
{intl.getMessage('install_auth_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
<div className={cn(theme.install.text, theme.install.text_block)}>
{intl.getMessage('install_auth_description')}
</div>
<Input
@@ -48,7 +48,7 @@ const Auth: FC<AuthProps> = observer(({
currentStep={2}
values={values}
/>
</div>
</>
);
});

View File

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

View File

@@ -1,5 +1,5 @@
import React, { FC, useContext } from 'react';
import { Tabs } from 'antd';
import { Tabs, Grid } from 'antd';
import cn from 'classnames';
import { FormikHelpers } from 'formik';
@@ -10,8 +10,8 @@ import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/in
import { FormValues } from '../../Install';
import StepButtons from '../StepButtons';
import s from './ConfigureDevices.module.pcss';
const { useBreakpoint } = Grid;
const { TabPane } = Tabs;
interface ConfigureDevicesProps {
@@ -23,10 +23,18 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
values, setFieldValue,
}) => {
const { ui: { intl }, install: { addresses } } = useContext(Store);
const screens = useBreakpoint();
const tabsPosition = screens.md ? 'left' : 'top';
const dhcp = (e: string) => (
// TODO: link to dhcp
<a href="http://" target="_blank" rel="noopener noreferrer">{e}</a>
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}
>
{e}
</a>
);
const allIps = addresses?.interfaces.reduce<string[]>((all, data) => {
@@ -44,76 +52,92 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
? allIps : dnsIp;
return (
<div>
<div className={theme.typography.title}>
<>
<div className={theme.install.title}>
{intl.getMessage('install_configure_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
<div className={cn(theme.install.text, theme.install.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)}>
<Tabs defaultActiveKey="1" tabPosition={tabsPosition} className={theme.install.tabs}>
<TabPane tab={intl.getMessage('router')} key="1">
<div className={theme.install.subtitle}>
{intl.getMessage('install_configure_how_to_title', { value: intl.getMessage('router') })}
</div>
<div className={cn(theme.install.text, theme.install.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)}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_configure_how_to_title', { value: 'Windows' })}
</div>
<div className={cn(theme.install.text, theme.install.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)}>
<TabPane tab="macOS" key="3">
<div className={theme.install.subtitle}>
{intl.getMessage('install_configure_how_to_title', { value: 'macOS' })}
</div>
<div className={cn(theme.install.text, theme.install.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)}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_configure_how_to_title', { value: 'Linux' })}
</div>
<div className={cn(theme.install.text, theme.install.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)}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_configure_how_to_title', { value: 'Android' })}
</div>
<div className={cn(theme.install.text, theme.install.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)}>
<TabPane tab="iOS" key="6">
<div className={theme.install.subtitle}>
{intl.getMessage('install_configure_how_to_title', { value: 'iOS' })}
</div>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_configure_ios', { p })}
</div>
</TabPane>
</Tabs>
<div className={theme.typography.subTitle}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_configure_adresses')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
<p>
<div className={cn(theme.install.text, theme.install.text_block)}>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_admin_interface_title')}
</p>
<p>
</div>
<div className={cn(theme.install.text, theme.install.text_base)}>
{selectedWebIps?.map((ip) => (
<div key={ip}>
<div key={ip} className={theme.install.ip}>
{ip}{values.web.port !== DEFAULT_IP_PORT && `:${values.web.port}`}
</div>
))}
</p>
<p>
</div>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_dns_server_title')}
</p>
<div>
</div>
<div className={cn(theme.install.text, theme.install.text_base)}>
{selectedDnsIps?.map((ip) => (
<div key={ip}>
<div key={ip} className={theme.install.ip}>
{ip}{values.dns.port !== DEFAULT_DNS_PORT && `:${values.dns.port}`}
</div>
))}
</div>
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_configure_dhcp', { dhcp })}
</div>
<StepButtons
@@ -121,7 +145,7 @@ const ConfigureDevices: FC<ConfigureDevicesProps> = ({
currentStep={4}
values={values}
/>
</div>
</>
);
};

View File

@@ -1,12 +0,0 @@
.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

@@ -9,7 +9,6 @@ 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';
@@ -39,7 +38,7 @@ const DnsServer: FC<DnsServerProps> = observer(({
};
const getManualBlock = () => (
<div className={s.manualOptions}>
<div className={theme.install.options}>
{addresses?.interfaces.map((a) => {
let name = '';
const type = chechNetworkType(a.name);
@@ -56,29 +55,27 @@ const DnsServer: FC<DnsServerProps> = observer(({
}
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 className={theme.install.name}>
{name}
</div>
{a.ipAddresses?.map((addrIp) => (
<div key={addrIp} className={theme.install.option}>
<div className={theme.install.address}>
{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>
);
})}
@@ -87,16 +84,16 @@ const DnsServer: FC<DnsServerProps> = observer(({
return (
<div>
<div className={theme.typography.title}>
<div className={theme.install.title}>
{intl.getMessage('install_dns_server_title')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_block)}>
<div className={cn(theme.install.text, theme.install.text_block)}>
{intl.getMessage('install_dns_server_desc')}
</div>
<div className={theme.typography.subTitle}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_dns_server_network_interfaces')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_dns_server_network_interfaces_desc')}
</div>
<Radio
@@ -116,10 +113,10 @@ const DnsServer: FC<DnsServerProps> = observer(({
]}
/>
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
<div className={theme.typography.subTitle}>
<div className={theme.install.subtitle}>
{intl.getMessage('install_dns_server_port')}
</div>
<div className={cn(theme.typography.text, theme.typography.text_base)}>
<div className={cn(theme.install.text, theme.install.text_base)}>
{intl.getMessage('install_dns_server_port_desc')}
</div>
<Input

View File

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

View File

@@ -1,13 +1,12 @@
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 theme from 'Lib/theme';
import { FormValues } from '../../Install';
import s from './StepButtons.module.pcss';
interface StepButtonsProps {
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
@@ -21,11 +20,11 @@ const StepButtons: FC<StepButtonsProps> = observer(({
}) => {
const { ui: { intl } } = useContext(Store);
return (
<div>
<div className={theme.install.actions}>
<Button
size="large"
type="ghost"
className={cn(s.button, s.inGroup)}
className={theme.install.button}
onClick={() => setFieldValue('step', currentStep - 1)}
>
{intl.getMessage('back')}
@@ -34,7 +33,7 @@ const StepButtons: FC<StepButtonsProps> = observer(({
size="large"
type="primary"
htmlType="submit"
className={cn(s.button)}
className={theme.install.button}
>
{intl.getMessage('next')}
</Button>

View File

@@ -1,3 +1,66 @@
.stepper {
margin-bottom: 48px;
}
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
height: 16px;
margin-bottom: 32px;
@media (--m-viewport) {
margin-bottom: 48px;
}
}
.wrap {
flex: 1;
position: relative;
display: inline-flex;
align-items: center;
justify-content: flex-end;
height: 16px;
&:before {
content: "";
position: absolute;
left: 0;
bottom: 7px;
width: 100%;
height: 1px;
background-color: var(--gray400);
}
&:first-child {
flex: 0;
&:before {
display: none;
}
}
&.current .circle {
transform: scale(2);
background-color: var(--green400);
border-color: var(--green400);
}
&.active .circle {
background-color: var(--green400);
border-color: var(--green400);
}
&.current:before,
&.active:before {
background-color: var(--green400);
}
}
.circle {
position: relative;
z-index: 1;
width: 8px;
height: 8px;
background-color: var(--white);
border-radius: 50%;
border: 1px solid var(--gray400);
transition: var(--transition) transform, var(--transition) background, var(--transition) border;
}

View File

@@ -1,23 +1,34 @@
import React, { FC } from 'react';
import { Steps } from 'antd';
import cn from 'classnames';
import s from './Stepper.module.pcss';
interface StepProps {
active: boolean;
current: boolean;
}
const Step: FC<StepProps> = ({ active, current }) => {
return (
<div className={cn(s.wrap, { [s.active]: active, [s.current]: current })}>
<div className={s.circle} />
</div>
);
};
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>
<div className={s.stepper}>
<Step current={currentStep === 0} active={currentStep >= 0} />
<Step current={currentStep === 1} active={currentStep >= 1} />
<Step current={currentStep === 2} active={currentStep >= 2} />
<Step current={currentStep === 3} active={currentStep >= 3} />
<Step current={currentStep === 4} active={currentStep >= 4} />
</div>
);
};

View File

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

View File

@@ -6,8 +6,6 @@ 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;
}
@@ -15,25 +13,25 @@ interface WelcomeProps {
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}>
<>
<Icon icon="logo" className={theme.install.logo} />
<div className={theme.install.title}>
{intl.getMessage('install_wellcome_title')}
</div>
<div className={theme.typography.text}>
<div className={theme.install.text}>
{intl.getMessage('install_wellcome_desc')}
</div>
<Button
size="large"
type="primary"
className={s.button}
onClick={onNext}
>
{intl.getMessage('install_wellcome_button')}
</Button>
</div>
<div className={theme.install.actions}>
<Button
size="large"
type="primary"
className={theme.install.button}
onClick={onNext}
>
{intl.getMessage('install_wellcome_button')}
</Button>
</div>
</>
);
});