-
copyright © {this.getYear()}
AdGuard
+
+
+ )}
+
);
}
}
+Footer.propTypes = {
+ dnsVersion: PropTypes.string,
+ processingVersion: PropTypes.bool,
+ getVersion: PropTypes.func,
+};
+
export default withNamespaces()(Footer);
diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js
index ef2fbf85..1655bc8e 100644
Binary files a/client/src/components/ui/Icons.js and b/client/src/components/ui/Icons.js differ
diff --git a/client/src/components/ui/PageTitle.css b/client/src/components/ui/PageTitle.css
index edcaa00b..bf698729 100644
--- a/client/src/components/ui/PageTitle.css
+++ b/client/src/components/ui/PageTitle.css
@@ -4,7 +4,13 @@
}
.page-title__actions {
- display: inline-block;
- vertical-align: baseline;
- margin-left: 20px;
+ display: block;
+}
+
+@media screen and (min-width: 768px) {
+ .page-title__actions {
+ display: inline-block;
+ vertical-align: baseline;
+ margin-left: 20px;
+ }
}
diff --git a/client/src/components/ui/Tabler.css b/client/src/components/ui/Tabler.css
index afe6dbc1..5a4482f7 100644
--- a/client/src/components/ui/Tabler.css
+++ b/client/src/components/ui/Tabler.css
@@ -78,7 +78,7 @@ section {
body {
margin: 0;
- font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
font-size: 0.9375rem;
font-weight: 400;
line-height: 1.5;
diff --git a/client/src/components/ui/Version.css b/client/src/components/ui/Version.css
new file mode 100644
index 00000000..5477d5f0
--- /dev/null
+++ b/client/src/components/ui/Version.css
@@ -0,0 +1,40 @@
+
+.version {
+ font-size: 0.80rem;
+}
+
+@media screen and (min-width: 1280px) {
+ .version {
+ font-size: 0.85rem;
+ }
+}
+
+.version__value {
+ font-weight: 600;
+}
+
+@media screen and (min-width: 992px) {
+ .version__value {
+ max-width: 100%;
+ overflow: visible;
+ }
+}
+
+.version__link {
+ position: relative;
+ display: inline-block;
+ border-bottom: 1px dashed #495057;
+ cursor: pointer;
+}
+
+.version__text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+@media screen and (min-width: 992px) {
+ .version__text {
+ justify-content: flex-end;
+ }
+}
diff --git a/client/src/components/Header/Version.js b/client/src/components/ui/Version.js
similarity index 53%
rename from client/src/components/Header/Version.js
rename to client/src/components/ui/Version.js
index 042b47a9..da651dc7 100644
--- a/client/src/components/Header/Version.js
+++ b/client/src/components/ui/Version.js
@@ -2,15 +2,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
+import './Version.css';
+
const Version = (props) => {
const {
- dnsVersion, dnsAddresses, processingVersion, t,
+ dnsVersion, processingVersion, t,
} = props;
return (
-
-
-
version:
{dnsVersion}
+
+
+ version: {dnsVersion}
-
-
- dns_addresses
-
-
-
- {dnsAddresses.map(ip =>
{ip})}
-
-
-
);
};
Version.propTypes = {
dnsVersion: PropTypes.string.isRequired,
- dnsAddresses: PropTypes.array.isRequired,
- dnsPort: PropTypes.number.isRequired,
getVersion: PropTypes.func.isRequired,
processingVersion: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
diff --git a/client/src/containers/Header.js b/client/src/containers/Header.js
index c253ac66..f230bc33 100644
--- a/client/src/containers/Header.js
+++ b/client/src/containers/Header.js
@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
-import * as actionCreators from '../actions';
+import { getVersion } from '../actions';
import Header from '../components/Header';
const mapStateToProps = (state) => {
@@ -8,7 +8,11 @@ const mapStateToProps = (state) => {
return props;
};
+const mapDispatchToProps = {
+ getVersion,
+};
+
export default connect(
mapStateToProps,
- actionCreators,
+ mapDispatchToProps,
)(Header);
diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js
index cac9882e..3bd94291 100644
--- a/client/src/helpers/form.js
+++ b/client/src/helpers/form.js
@@ -10,6 +10,7 @@ export const renderField = ({
placeholder,
type,
disabled,
+ autoComplete,
meta: { touched, error },
}) => (
@@ -20,6 +21,7 @@ export const renderField = ({
type={type}
className={className}
disabled={disabled}
+ autoComplete={autoComplete}
/>
{!disabled &&
touched &&
diff --git a/client/src/install/Setup/Setup.css b/client/src/install/Setup/Setup.css
index b71c5f55..11ee1430 100644
--- a/client/src/install/Setup/Setup.css
+++ b/client/src/install/Setup/Setup.css
@@ -1,5 +1,5 @@
.setup {
- min-height: calc(100vh - 80px);
+ min-height: calc(100vh - 71px);
line-height: 1.48;
}
diff --git a/client/src/login/Login/Form.js b/client/src/login/Login/Form.js
new file mode 100644
index 00000000..0524129c
--- /dev/null
+++ b/client/src/login/Login/Form.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Field, reduxForm } from 'redux-form';
+import { Trans, withNamespaces } from 'react-i18next';
+import flow from 'lodash/flow';
+
+import { renderField, required } from '../../helpers/form';
+
+const Form = (props) => {
+ const {
+ handleSubmit, processing, invalid, t,
+ } = props;
+
+ return (
+
+ );
+};
+
+Form.propTypes = {
+ handleSubmit: PropTypes.func.isRequired,
+ submitting: PropTypes.bool.isRequired,
+ invalid: PropTypes.bool.isRequired,
+ processing: PropTypes.bool.isRequired,
+ t: PropTypes.func.isRequired,
+};
+
+export default flow([
+ withNamespaces(),
+ reduxForm({
+ form: 'loginForm',
+ }),
+])(Form);
diff --git a/client/src/login/Login/Login.css b/client/src/login/Login/Login.css
new file mode 100644
index 00000000..628ff62b
--- /dev/null
+++ b/client/src/login/Login/Login.css
@@ -0,0 +1,47 @@
+.login {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: stretch;
+ min-height: 100vh;
+}
+
+.login__form {
+ margin: auto;
+ padding: 40px 15px 100px;
+ width: 100%;
+ max-width: 24rem;
+}
+
+.login__info {
+ position: relative;
+ text-align: center;
+}
+
+.login__message,
+.login__link {
+ font-size: 14px;
+ font-weight: 400;
+ letter-spacing: 0;
+}
+
+@media screen and (min-width: 992px) {
+ .login__message {
+ position: absolute;
+ top: 40px;
+ padding: 0 15px;
+ }
+}
+
+.form__group {
+ position: relative;
+ margin-bottom: 15px;
+}
+
+.form__message {
+ font-size: 11px;
+}
+
+.form__message--error {
+ color: #cd201f;
+}
diff --git a/client/src/login/Login/index.js b/client/src/login/Login/index.js
new file mode 100644
index 00000000..424a7a9d
--- /dev/null
+++ b/client/src/login/Login/index.js
@@ -0,0 +1,90 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import flow from 'lodash/flow';
+import { withNamespaces, Trans } from 'react-i18next';
+
+import * as actionCreators from '../../actions/login';
+import logo from '../../components/ui/svg/logo.svg';
+import Toasts from '../../components/Toasts';
+import Footer from '../../components/ui/Footer';
+import Form from './Form';
+
+import './Login.css';
+import '../../components/ui/Tabler.css';
+
+class Login extends Component {
+ state = {
+ isForgotPasswordVisible: false,
+ };
+
+ handleSubmit = ({ username: name, password }) => {
+ this.props.processLogin({ name, password });
+ };
+
+ toggleText = () => {
+ this.setState(prevState => ({
+ isForgotPasswordVisible: !prevState.isForgotPasswordVisible,
+ }));
+ };
+
+ render() {
+ const { processingLogin } = this.props.login;
+ const { isForgotPasswordVisible } = this.state;
+
+ return (
+
+
+
+

+
+
+
+
+ {isForgotPasswordVisible && (
+
+
+ link
+ ,
+ ]}
+ >
+ forgot_password_desc
+
+
+ )}
+
+
+
+
+
+ );
+ }
+}
+
+Login.propTypes = {
+ login: PropTypes.object.isRequired,
+ processLogin: PropTypes.func.isRequired,
+};
+
+const mapStateToProps = ({ login, toasts }) => ({ login, toasts });
+
+export default flow([
+ withNamespaces(),
+ connect(
+ mapStateToProps,
+ actionCreators,
+ ),
+])(Login);
diff --git a/client/src/login/index.js b/client/src/login/index.js
new file mode 100644
index 00000000..df35137a
--- /dev/null
+++ b/client/src/login/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/login';
+import '../i18n';
+import Login from './Login';
+
+const store = configureStore(reducers, {}); // set initial state
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root'),
+);
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 00a19816..589da42e 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -79,8 +79,8 @@ const dashboard = handleActions(
dnsVersion: version,
dnsPort,
dnsAddresses,
- upstreamDns: upstreamDns.join('\n'),
- bootstrapDns: bootstrapDns.join('\n'),
+ upstreamDns: (upstreamDns && upstreamDns.join('\n')) || '',
+ bootstrapDns: (bootstrapDns && bootstrapDns.join('\n')) || '',
allServers,
protectionEnabled,
language,
diff --git a/client/src/reducers/login.js b/client/src/reducers/login.js
new file mode 100644
index 00000000..e4c860a9
--- /dev/null
+++ b/client/src/reducers/login.js
@@ -0,0 +1,24 @@
+import { combineReducers } from 'redux';
+import { handleActions } from 'redux-actions';
+import { reducer as formReducer } from 'redux-form';
+
+import * as actions from '../actions/login';
+import toasts from './toasts';
+
+const login = handleActions({
+ [actions.processLoginRequest]: state => ({ ...state, processingLogin: true }),
+ [actions.processLoginFailure]: state => ({ ...state, processingLogin: false }),
+ [actions.processLoginSuccess]: (state, { payload }) => ({
+ ...state, ...payload, processingLogin: false,
+ }),
+}, {
+ processingLogin: false,
+ email: '',
+ password: '',
+});
+
+export default combineReducers({
+ login,
+ toasts,
+ form: formReducer,
+});
diff --git a/client/webpack.common.js b/client/webpack.common.js
index 350fca12..32248b14 100644
--- a/client/webpack.common.js
+++ b/client/webpack.common.js
@@ -10,8 +10,10 @@ const CopyPlugin = require('copy-webpack-plugin');
const RESOURCES_PATH = path.resolve(__dirname);
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/index.js');
+const ENTRY_LOGIN = path.resolve(RESOURCES_PATH, 'src/login/index.js');
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
+const HTML_LOGIN_PATH = path.resolve(RESOURCES_PATH, 'public/login.html');
const FAVICON_PATH = path.resolve(RESOURCES_PATH, 'public/favicon.png');
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
@@ -22,6 +24,7 @@ const config = {
entry: {
main: ENTRY_REACT,
install: ENTRY_INSTALL,
+ login: ENTRY_LOGIN,
},
output: {
path: PUBLIC_PATH,
@@ -116,6 +119,13 @@ const config = {
filename: 'install.html',
template: HTML_INSTALL_PATH,
}),
+ new HtmlWebpackPlugin({
+ inject: true,
+ cache: false,
+ chunks: ['login'],
+ filename: 'login.html',
+ template: HTML_LOGIN_PATH,
+ }),
new ExtractTextPlugin({
filename: '[name].[contenthash].css',
}),