Added components for web setup

This commit is contained in:
Ildar Kamalov 2019-01-18 20:17:48 +03:00 committed by Eugene Bujak
parent 71259c5f19
commit 5349ec76fd
31 changed files with 1144 additions and 15 deletions

View File

@ -157,5 +157,35 @@
"category_label": "Category", "category_label": "Category",
"rule_label": "Rule", "rule_label": "Rule",
"filter_label": "Filter", "filter_label": "Filter",
"unknown_filter": "Unknown filter {{filterId}}" "unknown_filter": "Unknown filter {{filterId}}",
"install_welcome_title": "Welcome to AdGuard Home!",
"install_welcome_desc": "Lorem ipsum dolor sit amet consectetur adipisicing elit.",
"install_settings_title": "Admin Web Interface",
"install_settings_listen": "Listen interface",
"install_settings_port": "Port",
"install_settings_interface_link": "Your AdGuard Home admin web interface is available on {{link}}",
"form_error_port": "Enter valid port value",
"install_settings_dns": "DNS server",
"install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server at {{ip}}",
"install_auth_title": "Authentication",
"install_auth_desc": "It is highly recommended to configure password authentication to your AdGuard Home admin web interface. Even if it is accessible only in your local network, it is still important to have it protected from unrestricted access.",
"install_auth_username": "Username",
"install_auth_password": "Password",
"install_auth_confirm": "Confirm password",
"install_auth_username_enter": "Enter username",
"install_auth_password_enter": "Enter password",
"install_step": "Step",
"install_devices_title": "Configure your devices",
"install_devices_desc": "In order for AdGuard Home to start working, you need to configure your devices to use it.",
"install_submit_title": "Congratulations!",
"install_submit_desc": "The setup procedure is finished and you are ready to start using AdGuard Home.",
"install_decices_router": "Router",
"install_decices_router_desc": "This setup will automatically cover all the devices connected to your home routerm and you will not need to configure each of them manually.",
"install_decices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don t remember it, you can ofter reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.",
"install_decices_router_list_2": " Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
"install_decices_router_list_3": "Enter your AdGuard Home server addresses there.",
"get_started": "Get Started",
"next": "Next",
"open_dashboard": "Open Dashboard",
"install_saved": "All settings saved"
} }

View File

@ -0,0 +1,54 @@
import { createAction } from 'redux-actions';
import Api from '../api/Api';
const apiClient = new Api();
export const addErrorToast = createAction('ADD_ERROR_TOAST');
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
export const nextStep = createAction('NEXT_STEP');
export const prevStep = createAction('PREV_STEP');
export const getDefaultAddressesRequest = createAction('GET_DEFAULT_ADDRESSES_REQUEST');
export const getDefaultAddressesFailure = createAction('GET_DEFAULT_ADDRESSES_FAILURE');
export const getDefaultAddressesSuccess = createAction('GET_DEFAULT_ADDRESSES_SUCCESS');
export const getDefaultAddresses = () => async (dispatch) => {
dispatch(getDefaultAddressesRequest());
try {
const addresses = await apiClient.getDefaultAddresses();
dispatch(getDefaultAddressesSuccess(addresses));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getDefaultAddressesFailure());
}
};
export const setAllSettingsRequest = createAction('SET_ALL_SETTINGS_REQUEST');
export const setAllSettingsFailure = createAction('SET_ALL_SETTINGS_FAILURE');
export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');
export const setAllSettings = values => async (dispatch) => {
dispatch(setAllSettingsRequest());
try {
const {
web,
dns,
username,
password,
} = values;
const config = {
web,
dns,
username,
password,
};
await apiClient.setAllSettings(config);
dispatch(setAllSettingsSuccess());
dispatch(addSuccessToast('install_saved'));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setAllSettingsFailure());
}
};

View File

@ -336,4 +336,22 @@ export default class Api {
}; };
return this.makeRequest(path, method, parameters); return this.makeRequest(path, method, parameters);
} }
// Installation
GET_DEFAULT_ADDRESSES = { path: 'install/get_default_addresses', method: 'GET' };
SET_ALL_SETTINGS = { path: 'install/set_all_settings', method: 'POST' };
getDefaultAddresses() {
const { path, method } = this.GET_DEFAULT_ADDRESSES;
return this.makeRequest(path, method);
}
setAllSettings(config) {
const { path, method } = this.SET_ALL_SETTINGS;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
} }

View File

@ -1,7 +1,7 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
} }
.status { .status {
@ -26,3 +26,7 @@ body {
height: 3px; height: 3px;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%); background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
} }
.hidden {
display: none;
}

View File

@ -25,7 +25,7 @@ class UserRules extends Component {
<textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} /> <textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} />
<div className="card-actions"> <div className="card-actions">
<button <button
className="btn btn-success btn-standart" className="btn btn-success btn-standard"
type="submit" type="submit"
onClick={this.handleSubmit} onClick={this.handleSubmit}
> >

View File

@ -96,14 +96,14 @@ class Filters extends Component {
/> />
<div className="card-actions"> <div className="card-actions">
<button <button
className="btn btn-success btn-standart mr-2" className="btn btn-success btn-standard mr-2"
type="submit" type="submit"
onClick={this.props.toggleFilteringModal} onClick={this.props.toggleFilteringModal}
> >
<Trans>add_filter_btn</Trans> <Trans>add_filter_btn</Trans>
</button> </button>
<button <button
className="btn btn-primary btn-standart" className="btn btn-primary btn-standard"
type="submit" type="submit"
onClick={this.props.refreshFilters} onClick={this.props.refreshFilters}
disabled={processingRefreshFilters} disabled={processingRefreshFilters}

View File

@ -6,7 +6,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import Menu from './Menu'; import Menu from './Menu';
import Version from './Version'; import Version from './Version';
import logo from './logo.svg'; import logo from '../ui/svg/logo.svg';
import './Header.css'; import './Header.css';
class Header extends Component { class Header extends Component {

View File

@ -124,7 +124,7 @@ const Form = (props) => {
<button <button
type="submit" type="submit"
className="btn btn-success btn-standart" className="btn btn-success btn-standard"
disabled={pristine || submitting} disabled={pristine || submitting}
> >
{t('save_config')} {t('save_config')}

View File

@ -37,7 +37,7 @@ class Dhcp extends Component {
return ( return (
<button <button
type="button" type="button"
className="btn btn-standart mr-2 btn-gray" className="btn btn-standard mr-2 btn-gray"
onClick={() => this.props.toggleDhcp(config)} onClick={() => this.props.toggleDhcp(config)}
disabled={processingDhcp} disabled={processingDhcp}
> >
@ -49,7 +49,7 @@ class Dhcp extends Component {
return ( return (
<button <button
type="button" type="button"
className="btn btn-standart mr-2 btn-success" className="btn btn-standard mr-2 btn-success"
onClick={() => this.handleToggle(config)} onClick={() => this.handleToggle(config)}
disabled={!filledConfig || activeDhcpFound || processingDhcp} disabled={!filledConfig || activeDhcpFound || processingDhcp}
> >
@ -91,8 +91,8 @@ class Dhcp extends Component {
render() { render() {
const { t, dhcp } = this.props; const { t, dhcp } = this.props;
const statusButtonClass = classnames({ const statusButtonClass = classnames({
'btn btn-primary btn-standart': true, 'btn btn-primary btn-standard': true,
'btn btn-primary btn-standart btn-loading': dhcp.processingStatus, 'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
}); });
return ( return (

View File

@ -11,7 +11,7 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
.btn-standart { .btn-standard {
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
} }

View File

@ -21,8 +21,8 @@ class Upstream extends Component {
render() { render() {
const testButtonClass = classnames({ const testButtonClass = classnames({
'btn btn-primary btn-standart mr-2': true, 'btn btn-primary btn-standard mr-2': true,
'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream, 'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
}); });
const { t } = this.props; const { t } = this.props;
@ -49,7 +49,7 @@ class Upstream extends Component {
<Trans>test_upstream_btn</Trans> <Trans>test_upstream_btn</Trans>
</button> </button>
<button <button
className="btn btn-success btn-standart" className="btn btn-success btn-standard"
type="submit" type="submit"
onClick={this.handleSubmit} onClick={this.handleSubmit}
> >

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
class Tab extends Component {
handleClick = () => {
this.props.onClick(this.props.label);
}
render() {
const {
activeTab,
label,
} = this.props;
const tabClass = classnames({
tab__control: true,
'tab__control--active': activeTab === label,
});
return (
<div
className={tabClass}
onClick={this.handleClick}
>
<svg className="tab__icon">
<use xlinkHref={`#${label.toLowerCase()}`} />
</svg>
{label}
</div>
);
}
}
Tab.propTypes = {
activeTab: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};
export default Tab;

View File

@ -0,0 +1,42 @@
.tabs__controls {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
padding: 15px 0;
border-bottom: 1px solid #e8e8e8;
}
.tab__control {
display: flex;
flex-direction: column;
align-items: center;
min-width: 70px;
font-size: 13px;
color: #555555;
cursor: pointer;
opacity: 0.6;
}
.tab__control:hover,
.tab__control:focus {
opacity: 1;
}
.tab__control--active {
font-weight: 700;
color: #4a4a4a;
opacity: 1;
}
.tab__title {
margin-bottom: 10px;
font-size: 16px;
font-weight: 700;
}
.tab__icon {
width: 24px;
height: 24px;
margin-bottom: 6px;
fill: #4a4a4a;
}

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Tab from './Tab';
import './Tabs.css';
class Tabs extends Component {
state = {
activeTab: this.props.children[0].props.label,
};
onClickTabControl = (tab) => {
this.setState({ activeTab: tab });
}
render() {
const {
props: {
children,
},
state: {
activeTab,
},
} = this;
return (
<div className="tabs">
<div className="tabs__controls">
{children.map((child) => {
const { label } = child.props;
return (
<Tab
activeTab={activeTab}
key={label}
label={label}
onClick={this.onClickTabControl}
/>
);
})}
</div>
<div className="tabs__content">
{children.map((child) => {
if (child.props.label !== activeTab) {
return false;
}
return child.props.children;
})}
</div>
</div>
);
}
}
Tabs.propTypes = {
children: PropTypes.array.isRequired,
};
export default Tabs;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -60,3 +60,6 @@ export const LANGUAGES = [
name: '正體中文', name: '正體中文',
}, },
]; ];
export const INSTALL_FIRST_STEP = 1;
export const INSTALL_TOTAL_STEPS = 5;

View File

@ -0,0 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import flow from 'lodash/flow';
import Controls from './Controls';
import validate from './validate';
import renderField from './renderField';
const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
const Auth = (props) => {
const {
handleSubmit,
submitting,
pristine,
t,
} = props;
return (
<form className="setup__step" onSubmit={handleSubmit}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_auth_title</Trans>
</div>
<p className="setup__desc">
<Trans>install_auth_desc</Trans>
</p>
<div className="form-group">
<label>
<Trans>install_auth_username</Trans>
</label>
<Field
name="username"
component={renderField}
type="text"
className="form-control"
placeholder={ t('install_auth_username_enter') }
validate={[required]}
autoComplete="username"
/>
</div>
<div className="form-group">
<label>
<Trans>install_auth_password</Trans>
</label>
<Field
name="password"
component={renderField}
type="password"
className="form-control"
placeholder={ t('install_auth_password_enter') }
validate={[required]}
autoComplete="new-password"
/>
</div>
<div className="form-group">
<label>
<Trans>install_auth_confirm</Trans>
</label>
<Field
name="confirm_password"
component={renderField}
type="password"
className="form-control"
placeholder={ t('install_auth_confirm') }
validate={[required]}
autoComplete="new-password"
/>
</div>
</div>
<Controls submitting={submitting} pristine={pristine} />
</form>
);
};
Auth.propTypes = {
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
};
export default flow([
withNamespaces(),
reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
validate,
}),
])(Auth);

View File

@ -0,0 +1,115 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import * as actionCreators from '../../actions/install';
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS } from '../../helpers/constants';
class Controls extends Component {
nextStep = () => {
if (this.props.step < INSTALL_TOTAL_STEPS) {
this.props.nextStep();
}
}
prevStep = () => {
if (this.props.step > INSTALL_FIRST_STEP) {
this.props.prevStep();
}
}
renderButtons(step) {
switch (step) {
case 1:
return (
<button
type="button"
className="btn btn-success btn-standard btn-lg"
onClick={this.props.nextStep}
>
<Trans>get_started</Trans>
</button>
);
case 2:
case 3:
return (
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard btn-lg"
onClick={this.props.prevStep}
>
<Trans>back</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard btn-lg"
disabled={this.props.submitting || this.props.pristine}
>
<Trans>next</Trans>
</button>
</div>
);
case 4:
return (
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard btn-lg"
onClick={this.props.prevStep}
>
<Trans>back</Trans>
</button>
<button
type="button"
className="btn btn-success btn-standard btn-lg"
onClick={this.props.nextStep}
disabled={this.props.submitting || this.props.pristine}
>
<Trans>next</Trans>
</button>
</div>
);
case 5:
return (
<button
type="submit"
className="btn btn-success btn-standard btn-lg"
disabled={this.props.submitting || this.props.pristine}
>
<Trans>open_dashboard</Trans>
</button>
);
default:
return false;
}
}
render() {
return (
<div className="setup__nav">
{this.renderButtons(this.props.step)}
</div>
);
}
}
Controls.propTypes = {
step: PropTypes.number.isRequired,
nextStep: PropTypes.func,
prevStep: PropTypes.func,
pristine: PropTypes.bool,
submitting: PropTypes.bool,
};
const mapStateToProps = (state) => {
const { step } = state.install;
const props = { step };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Controls);

View File

@ -0,0 +1,68 @@
import React from 'react';
import { Trans } from 'react-i18next';
import Tabs from '../../components/ui/Tabs';
import Icons from '../../components/ui/Icons';
import Controls from './Controls';
const Devices = () => (
<div className="setup__step">
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_devices_title</Trans>
</div>
<p className="setup__desc">
<Trans>install_devices_desc</Trans>
</p>
<Icons />
<Tabs>
<div label="Router">
<div className="tab__title">
<Trans>install_decices_router</Trans>
</div>
<div className="tab__text">
<Trans>install_decices_router_desc</Trans>
<ol>
<li>
<Trans>install_decices_router_list_1</Trans>
</li>
<li>
<Trans>install_decices_router_list_2</Trans>
</li>
<li>
<Trans>install_decices_router_list_3</Trans>
</li>
</ol>
</div>
</div>
<div label="Windows">
<div className="tab__title">
Windows
</div>
<div className="tab__text">Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti sapiente magnam autem excepturi repellendus, voluptatem officia sint quas nulla maiores velit odit dolore commodi quia reprehenderit vero repudiandae adipisci aliquam.</div>
</div>
<div label="macOS">
<div className="tab__title">
macOS
</div>
<div className="tab__text">Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti sapiente magnam autem excepturi repellendus, voluptatem officia sint quas nulla maiores velit odit dolore commodi quia reprehenderit vero repudiandae adipisci aliquam.</div>
</div>
<div label="Android">
<div className="tab__title">
Android
</div>
<div className="tab__text">Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti sapiente magnam autem excepturi repellendus, voluptatem officia sint quas nulla maiores velit odit dolore commodi quia reprehenderit vero repudiandae adipisci aliquam.</div>
</div>
<div label="iOS">
<div className="tab__title">
iOS
</div>
<div className="tab__text">Lorem ipsum dolor sit amet consectetur adipisicing elit. Deleniti sapiente magnam autem excepturi repellendus, voluptatem officia sint quas nulla maiores velit odit dolore commodi quia reprehenderit vero repudiandae adipisci aliquam.</div>
</div>
</Tabs>
</div>
<Controls />
</div>
);
export default Devices;

View File

@ -0,0 +1,23 @@
import React, { Component } from 'react';
import { Trans } from 'react-i18next';
import Controls from './Controls';
class Greeting extends Component {
render() {
return (
<div className="setup__step">
<div className="setup__group">
<h1 className="setup__title">
<Trans>install_welcome_title</Trans>
</h1>
<p className="setup__desc">
<Trans>install_welcome_desc</Trans>
</p>
</div>
<Controls />
</div>
);
}
}
export default Greeting;

View File

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import { INSTALL_TOTAL_STEPS } from '../../helpers/constants';
const getProgressPercent = step => (step / INSTALL_TOTAL_STEPS) * 100;
const Progress = props => (
<div className="setup__progress">
<Trans>install_step</Trans> {props.step}/{INSTALL_TOTAL_STEPS}
<div className="setup__progress-wrap">
<div
className="setup__progress-inner"
style={{ width: `${getProgressPercent(props.step)}%` }}
/>
</div>
</div>
);
Progress.propTypes = {
step: PropTypes.number.isRequired,
};
export default Progress;

View File

@ -0,0 +1,160 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans } from 'react-i18next';
import Controls from './Controls';
import renderField from './renderField';
import { R_IPV4 } from '../../helpers/constants';
const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
};
const port = (value) => {
if (value < 1 || value > 65535) {
return <Trans>form_error_port</Trans>;
}
return false;
};
const toNumber = value => value && parseInt(value, 10);
let Settings = (props) => {
const {
handleSubmit,
interfaceIp,
dnsIp,
} = props;
return (
<form className="setup__step" onSubmit={handleSubmit}>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>install_settings_title</Trans>
</div>
<div className="row">
<div className="col-8">
<div className="form-group">
<label>
<Trans>install_settings_listen</Trans>
</label>
<Field
name="web.ip"
component={renderField}
type="text"
className="form-control"
placeholder="0.0.0.0"
validate={[ipv4, required]}
/>
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="web.port"
component={renderField}
type="number"
className="form-control"
placeholder="80"
validate={[port, required]}
normalize={toNumber}
/>
</div>
</div>
</div>
<div className="setup__desc">
<Trans>install_settings_interface_link</Trans> <a href={`http://${interfaceIp}`}>{`http://${interfaceIp}`}</a>
</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={renderField}
type="text"
className="form-control"
placeholder="0.0.0.0"
validate={[ipv4, required]}
/>
</div>
</div>
<div className="col-4">
<div className="form-group">
<label>
<Trans>install_settings_port</Trans>
</label>
<Field
name="dns.port"
component={renderField}
type="number"
className="form-control"
placeholder="80"
validate={[port, required]}
normalize={toNumber}
/>
</div>
</div>
</div>
<p className="setup__desc">
<Trans>install_settings_dns_desc</Trans> <strong>{dnsIp}</strong>
</p>
</div>
<Controls />
</form>
);
};
Settings.propTypes = {
handleSubmit: PropTypes.func.isRequired,
interfaceIp: PropTypes.string.isRequired,
dnsIp: PropTypes.string.isRequired,
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
initialValues: PropTypes.object,
};
Settings.defaultProps = {
interfaceIp: '192.168.0.1',
dnsIp: '192.168.0.1',
};
const selector = formValueSelector('install');
Settings = connect((state) => {
const interfaceIp = selector(state, 'web.ip');
const dnsIp = selector(state, 'dns.ip');
return {
interfaceIp,
dnsIp,
};
})(Settings);
export default reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
})(Settings);

View File

@ -0,0 +1,105 @@
.setup {
min-height: calc(100vh - 80px);
padding: 50px 0;
line-height: 1.48;
}
.setup__container {
max-width: 650px;
margin: 0 auto;
padding: 30px 20px;
line-height: 1.6;
background-color: #fff;
box-shadow: 0 1px 4px rgba(74, 74, 74, .36);
border-radius: 3px;
}
@media screen and (min-width: 768px) {
.setup__container {
width: 650px;
padding: 40px 30px;
}
}
.setup__logo {
display: block;
margin: 0 auto 40px;
max-width: 140px;
}
.setup__nav {
text-align: center;
}
.setup__step {
margin-bottom: 25px;
}
.setup__title {
margin-bottom: 30px;
font-size: 28px;
text-align: center;
font-weight: 700;
}
.setup__subtitle {
margin-bottom: 10px;
font-size: 17px;
font-weight: 700;
}
.setup__desc {
font-size: 15px;
}
.setup__group {
margin-bottom: 35px;
}
.setup__group:last-child {
margin-bottom: 0;
}
.setup__progress {
font-size: 13px;
text-align: center;
}
.setup__progress-wrap {
height: 4px;
margin: 20px -20px -30px -20px;
overflow: hidden;
background-color: #eaeaea;
border-radius: 0 0 3px 3px;
}
@media screen and (min-width: 768px) {
.setup__progress-wrap {
margin: 20px -30px -40px -30px;
}
}
.setup__progress-inner {
width: 0;
height: 100%;
font-size: 1.2rem;
line-height: 20px;
color: #fff;
text-align: center;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
transition: width 0.6s ease;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}
.btn-standard {
padding-left: 20px;
padding-right: 20px;
}
.form__message {
font-size: 11px;
}
.form__message--error {
color: #cd201f;
}

View File

@ -0,0 +1,44 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { reduxForm } from 'redux-form';
import { Trans } from 'react-i18next';
import Controls from './Controls';
class Submit extends Component {
render() {
const {
handleSubmit,
pristine,
submitting,
} = this.props;
return (
<div className="setup__step">
<div className="setup__group">
<h1 className="setup__title">
<Trans>install_submit_title</Trans>
</h1>
<p className="setup__desc">
<Trans>install_submit_desc</Trans>
</p>
</div>
<form onSubmit={handleSubmit}>
<Controls submitting={submitting} pristine={pristine} />
</form>
</div>
);
}
}
Submit.propTypes = {
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
};
export default reduxForm({
form: 'install',
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
})(Submit);

View File

@ -0,0 +1,115 @@
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as actionCreators from '../../actions/install';
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS } 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 Footer from '../../components/ui/Footer';
import logo from '../../components/ui/svg/logo.svg';
import './Setup.css';
import '../../components/ui/Tabler.css';
class Setup extends Component {
componentDidMount() {
this.props.getDefaultAddresses();
}
handleFormSubmit = (values) => {
this.props.setAllSettings(values);
};
nextStep = () => {
if (this.props.install.step < INSTALL_TOTAL_STEPS) {
this.props.nextStep();
}
}
prevStep = () => {
if (this.props.install.step > INSTALL_FIRST_STEP) {
this.props.prevStep();
}
}
renderPage(step, config) {
switch (step) {
case 1:
return <Greeting />;
case 2:
return (
<Settings
initialValues={config}
onSubmit={this.nextStep}
/>
);
case 3:
return (
<Auth onSubmit={this.nextStep} />
);
case 4:
return <Devices />;
case 5:
return <Submit onSubmit={this.handleFormSubmit} />;
default:
return false;
}
}
render() {
const {
processingDefault,
step,
web,
dns,
} = this.props.install;
return (
<Fragment>
{processingDefault && <Loading />}
{!processingDefault &&
<Fragment>
<div className="setup">
<div className="setup__container">
<img src={logo} className="setup__logo" alt="logo" />
{this.renderPage(step, { web, dns })}
<Progress step={step} />
</div>
</div>
<Footer />
</Fragment>
}
</Fragment>
);
}
}
Setup.propTypes = {
getDefaultAddresses: PropTypes.func.isRequired,
setAllSettings: PropTypes.func.isRequired,
nextStep: PropTypes.func.isRequired,
prevStep: PropTypes.func.isRequired,
install: PropTypes.object.isRequired,
step: PropTypes.number,
web: PropTypes.object,
dns: PropTypes.object,
};
const mapStateToProps = (state) => {
const { install } = state;
const props = { install };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Setup);

View File

@ -0,0 +1,19 @@
import React, { Fragment } from 'react';
const renderField = ({
input, className, placeholder, type, disabled, autoComplete, meta: { touched, error },
}) => (
<Fragment>
<input
{...input}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
autoComplete={autoComplete}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export default renderField;

View File

@ -0,0 +1,11 @@
const validate = (values) => {
const errors = {};
if (values.confirm_password !== values.password) {
errors.confirm_password = 'Password mismatched';
}
return errors;
};
export default validate;

View File

@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import '../components/App/index.css';
import '../components/ui/ReactTable.css';
import configureStore from '../configureStore';
import reducers from '../reducers/install';
import '../i18n';
import Setup from './Setup';
const store = configureStore(reducers, {}); // set initial state
ReactDOM.render(
<Provider store={store}>
<Setup />
</Provider>,
document.getElementById('root'),
);

View File

@ -0,0 +1,29 @@
import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions';
import { reducer as formReducer } from 'redux-form';
import * as actions from '../actions/install';
const install = handleActions({
[actions.getDefaultAddressesRequest]: state => ({ ...state, processingDefault: true }),
[actions.getDefaultAddressesFailure]: state => ({ ...state, processingDefault: false }),
[actions.getDefaultAddressesSuccess]: (state, { payload }) => {
const newState = { ...state, ...payload, processingDefault: false };
return newState;
},
[actions.nextStep]: state => ({ ...state, step: state.step + 1 }),
[actions.prevStep]: state => ({ ...state, step: state.step - 1 }),
[actions.setAllSettingsRequest]: state => ({ ...state, processingSubmit: true }),
[actions.setAllSettingsFailure]: state => ({ ...state, processingSubmit: false }),
[actions.setAllSettingsSuccess]: state => ({ ...state, processingSubmit: false }),
}, {
step: 1,
processingDefault: true,
});
export default combineReducers({
install,
form: formReducer,
});