diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 10471501..2a9d73eb 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -157,5 +157,35 @@
"category_label": "Category",
"rule_label": "Rule",
"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"
}
\ No newline at end of file
diff --git a/client/src/actions/install.js b/client/src/actions/install.js
new file mode 100644
index 00000000..508aa8da
--- /dev/null
+++ b/client/src/actions/install.js
@@ -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());
+ }
+};
diff --git a/client/src/api/Api.js b/client/src/api/Api.js
index 4592fae7..7f1da9c5 100644
--- a/client/src/api/Api.js
+++ b/client/src/api/Api.js
@@ -336,4 +336,22 @@ export default class Api {
};
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);
+ }
}
diff --git a/client/src/components/App/index.css b/client/src/components/App/index.css
index 61ee53a4..391ddabd 100644
--- a/client/src/components/App/index.css
+++ b/client/src/components/App/index.css
@@ -1,7 +1,7 @@
body {
margin: 0;
padding: 0;
- font-family: sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
}
.status {
@@ -26,3 +26,7 @@ body {
height: 3px;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}
+
+.hidden {
+ display: none;
+}
diff --git a/client/src/components/Filters/UserRules.js b/client/src/components/Filters/UserRules.js
index 9aae0281..7a251f3a 100644
--- a/client/src/components/Filters/UserRules.js
+++ b/client/src/components/Filters/UserRules.js
@@ -25,7 +25,7 @@ class UserRules extends Component {
diff --git a/client/src/components/Filters/index.js b/client/src/components/Filters/index.js
index 339bc673..6b749e83 100644
--- a/client/src/components/Filters/index.js
+++ b/client/src/components/Filters/index.js
@@ -96,14 +96,14 @@ class Filters extends Component {
/>
add_filter_btn
{
{t('save_config')}
diff --git a/client/src/components/Settings/Dhcp/index.js b/client/src/components/Settings/Dhcp/index.js
index eed256a6..d46d8551 100644
--- a/client/src/components/Settings/Dhcp/index.js
+++ b/client/src/components/Settings/Dhcp/index.js
@@ -37,7 +37,7 @@ class Dhcp extends Component {
return (
this.props.toggleDhcp(config)}
disabled={processingDhcp}
>
@@ -49,7 +49,7 @@ class Dhcp extends Component {
return (
this.handleToggle(config)}
disabled={!filledConfig || activeDhcpFound || processingDhcp}
>
@@ -91,8 +91,8 @@ class Dhcp extends Component {
render() {
const { t, dhcp } = this.props;
const statusButtonClass = classnames({
- 'btn btn-primary btn-standart': true,
- 'btn btn-primary btn-standart btn-loading': dhcp.processingStatus,
+ 'btn btn-primary btn-standard': true,
+ 'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
});
return (
diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css
index a4ff2394..9530ef36 100644
--- a/client/src/components/Settings/Settings.css
+++ b/client/src/components/Settings/Settings.css
@@ -11,7 +11,7 @@
margin-bottom: 15px;
}
-.btn-standart {
+.btn-standard {
padding-left: 20px;
padding-right: 20px;
}
diff --git a/client/src/components/Settings/Upstream.js b/client/src/components/Settings/Upstream.js
index 4ae0598a..fe24c4d7 100644
--- a/client/src/components/Settings/Upstream.js
+++ b/client/src/components/Settings/Upstream.js
@@ -21,8 +21,8 @@ class Upstream extends Component {
render() {
const testButtonClass = classnames({
- 'btn btn-primary btn-standart mr-2': true,
- 'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream,
+ 'btn btn-primary btn-standard mr-2': true,
+ 'btn btn-primary btn-standard mr-2 btn-loading': this.props.processingTestUpstream,
});
const { t } = this.props;
@@ -49,7 +49,7 @@ class Upstream extends Component {
test_upstream_btn
diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js
new file mode 100644
index 00000000..dec6f0cd
Binary files /dev/null and b/client/src/components/ui/Icons.js differ
diff --git a/client/src/components/ui/Tab.js b/client/src/components/ui/Tab.js
new file mode 100644
index 00000000..b97e4f60
--- /dev/null
+++ b/client/src/components/ui/Tab.js
@@ -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 (
+
+
+
+
+ {label}
+
+ );
+ }
+}
+
+Tab.propTypes = {
+ activeTab: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+};
+
+export default Tab;
diff --git a/client/src/components/ui/Tabs.css b/client/src/components/ui/Tabs.css
new file mode 100644
index 00000000..141b2a7f
--- /dev/null
+++ b/client/src/components/ui/Tabs.css
@@ -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;
+}
diff --git a/client/src/components/ui/Tabs.js b/client/src/components/ui/Tabs.js
new file mode 100644
index 00000000..38546b98
--- /dev/null
+++ b/client/src/components/ui/Tabs.js
@@ -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 (
+
+
+ {children.map((child) => {
+ const { label } = child.props;
+
+ return (
+
+ );
+ })}
+
+
+ {children.map((child) => {
+ if (child.props.label !== activeTab) {
+ return false;
+ }
+ return child.props.children;
+ })}
+
+
+ );
+ }
+}
+
+Tabs.propTypes = {
+ children: PropTypes.array.isRequired,
+};
+
+export default Tabs;
diff --git a/client/src/components/ui/svg/icons.svg b/client/src/components/ui/svg/icons.svg
new file mode 100644
index 00000000..ad531ac5
--- /dev/null
+++ b/client/src/components/ui/svg/icons.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/Header/logo.svg b/client/src/components/ui/svg/logo.svg
similarity index 100%
rename from client/src/components/Header/logo.svg
rename to client/src/components/ui/svg/logo.svg
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index fb7bbe64..21e5a961 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -60,3 +60,6 @@ export const LANGUAGES = [
name: '正體中文',
},
];
+
+export const INSTALL_FIRST_STEP = 1;
+export const INSTALL_TOTAL_STEPS = 5;
diff --git a/client/src/install/Setup/Auth.js b/client/src/install/Setup/Auth.js
new file mode 100644
index 00000000..ce6ca495
--- /dev/null
+++ b/client/src/install/Setup/Auth.js
@@ -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 form_error_required ;
+};
+
+const Auth = (props) => {
+ const {
+ handleSubmit,
+ submitting,
+ pristine,
+ t,
+ } = props;
+
+ return (
+
+ );
+};
+
+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);
diff --git a/client/src/install/Setup/Controls.js b/client/src/install/Setup/Controls.js
new file mode 100644
index 00000000..37f5425b
--- /dev/null
+++ b/client/src/install/Setup/Controls.js
@@ -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 (
+
+ get_started
+
+ );
+ case 2:
+ case 3:
+ return (
+
+
+ back
+
+
+ next
+
+
+ );
+ case 4:
+ return (
+
+
+ back
+
+
+ next
+
+
+ );
+ case 5:
+ return (
+
+ open_dashboard
+
+ );
+ default:
+ return false;
+ }
+ }
+
+ render() {
+ return (
+
+ {this.renderButtons(this.props.step)}
+
+ );
+ }
+}
+
+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);
diff --git a/client/src/install/Setup/Devices.js b/client/src/install/Setup/Devices.js
new file mode 100644
index 00000000..389e9aa7
--- /dev/null
+++ b/client/src/install/Setup/Devices.js
@@ -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 = () => (
+
+
+
+ install_devices_title
+
+
+ install_devices_desc
+
+
+
+
+
+ install_decices_router
+
+
+
install_decices_router_desc
+
+
+ install_decices_router_list_1
+
+
+ install_decices_router_list_2
+
+
+ install_decices_router_list_3
+
+
+
+
+
+
+ Windows
+
+
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.
+
+
+
+ macOS
+
+
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.
+
+
+
+ Android
+
+
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.
+
+
+
+ iOS
+
+
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.
+
+
+
+
+
+);
+
+export default Devices;
diff --git a/client/src/install/Setup/Greeting.js b/client/src/install/Setup/Greeting.js
new file mode 100644
index 00000000..0398e5af
--- /dev/null
+++ b/client/src/install/Setup/Greeting.js
@@ -0,0 +1,23 @@
+import React, { Component } from 'react';
+import { Trans } from 'react-i18next';
+import Controls from './Controls';
+
+class Greeting extends Component {
+ render() {
+ return (
+
+
+
+ install_welcome_title
+
+
+ install_welcome_desc
+
+
+
+
+ );
+ }
+}
+
+export default Greeting;
diff --git a/client/src/install/Setup/Progress.js b/client/src/install/Setup/Progress.js
new file mode 100644
index 00000000..db44b8c4
--- /dev/null
+++ b/client/src/install/Setup/Progress.js
@@ -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 => (
+
+
install_step {props.step}/{INSTALL_TOTAL_STEPS}
+
+
+);
+
+Progress.propTypes = {
+ step: PropTypes.number.isRequired,
+};
+
+export default Progress;
diff --git a/client/src/install/Setup/Settings.js b/client/src/install/Setup/Settings.js
new file mode 100644
index 00000000..756a46b3
--- /dev/null
+++ b/client/src/install/Setup/Settings.js
@@ -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 form_error_required ;
+};
+
+const ipv4 = (value) => {
+ if (value && !new RegExp(R_IPV4).test(value)) {
+ return form_error_ip_format ;
+ }
+ return false;
+};
+
+const port = (value) => {
+ if (value < 1 || value > 65535) {
+ return form_error_port ;
+ }
+ return false;
+};
+
+const toNumber = value => value && parseInt(value, 10);
+
+let Settings = (props) => {
+ const {
+ handleSubmit,
+ interfaceIp,
+ dnsIp,
+ } = props;
+
+ return (
+
+ );
+};
+
+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);
diff --git a/client/src/install/Setup/Setup.css b/client/src/install/Setup/Setup.css
new file mode 100644
index 00000000..963458af
--- /dev/null
+++ b/client/src/install/Setup/Setup.css
@@ -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;
+}
diff --git a/client/src/install/Setup/Submit.js b/client/src/install/Setup/Submit.js
new file mode 100644
index 00000000..9f52a2df
--- /dev/null
+++ b/client/src/install/Setup/Submit.js
@@ -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 (
+
+
+
+ install_submit_title
+
+
+ install_submit_desc
+
+
+
+
+ );
+ }
+}
+
+Submit.propTypes = {
+ handleSubmit: PropTypes.func.isRequired,
+ pristine: PropTypes.bool.isRequired,
+ submitting: PropTypes.bool.isRequired,
+};
+
+export default reduxForm({
+ form: 'install',
+ destroyOnUnmount: false,
+ forceUnregisterOnUnmount: true,
+})(Submit);
diff --git a/client/src/install/Setup/index.js b/client/src/install/Setup/index.js
new file mode 100644
index 00000000..ed827edf
--- /dev/null
+++ b/client/src/install/Setup/index.js
@@ -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 ;
+ case 2:
+ return (
+
+ );
+ case 3:
+ return (
+
+ );
+ case 4:
+ return ;
+ case 5:
+ return ;
+ default:
+ return false;
+ }
+ }
+
+ render() {
+ const {
+ processingDefault,
+ step,
+ web,
+ dns,
+ } = this.props.install;
+
+ return (
+
+ {processingDefault && }
+ {!processingDefault &&
+
+
+
+
+ {this.renderPage(step, { web, dns })}
+
+
+
+
+
+ }
+
+ );
+ }
+}
+
+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);
diff --git a/client/src/install/Setup/renderField.js b/client/src/install/Setup/renderField.js
new file mode 100644
index 00000000..a323f17c
--- /dev/null
+++ b/client/src/install/Setup/renderField.js
@@ -0,0 +1,19 @@
+import React, { Fragment } from 'react';
+
+const renderField = ({
+ input, className, placeholder, type, disabled, autoComplete, meta: { touched, error },
+}) => (
+
+
+ {!disabled && touched && (error && {error} )}
+
+);
+
+export default renderField;
diff --git a/client/src/install/Setup/validate.js b/client/src/install/Setup/validate.js
new file mode 100644
index 00000000..8a3e1fca
--- /dev/null
+++ b/client/src/install/Setup/validate.js
@@ -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;
diff --git a/client/src/install/index.js b/client/src/install/index.js
new file mode 100644
index 00000000..2905bd4b
--- /dev/null
+++ b/client/src/install/index.js
@@ -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(
+
+
+ ,
+ document.getElementById('root'),
+);
diff --git a/client/src/reducers/install.js b/client/src/reducers/install.js
new file mode 100644
index 00000000..5d6bdf19
--- /dev/null
+++ b/client/src/reducers/install.js
@@ -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,
+});