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

@@ -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;