+ client: login page
This commit is contained in:
parent
1e4edf0669
commit
66bd06cf69
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
<meta name="google" content="notranslate">
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" sizes="48x48">
|
||||||
|
<title>Login</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You need to enable JavaScript to run this app.
|
||||||
|
</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -199,7 +199,7 @@
|
||||||
"install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server on the following addresses:",
|
"install_settings_dns_desc": "You will need to configure your devices or router to use the DNS server on the following addresses:",
|
||||||
"install_settings_all_interfaces": "All interfaces",
|
"install_settings_all_interfaces": "All interfaces",
|
||||||
"install_auth_title": "Authentication",
|
"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 to protect it from unrestricted access.",
|
"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 protect it from unrestricted access.",
|
||||||
"install_auth_username": "Username",
|
"install_auth_username": "Username",
|
||||||
"install_auth_password": "Password",
|
"install_auth_password": "Password",
|
||||||
"install_auth_confirm": "Confirm password",
|
"install_auth_confirm": "Confirm password",
|
||||||
|
@ -384,5 +384,13 @@
|
||||||
"filters_configuration": "Filters configuration",
|
"filters_configuration": "Filters configuration",
|
||||||
"filters_enable": "Enable filters",
|
"filters_enable": "Enable filters",
|
||||||
"filters_interval": "Filters update interval",
|
"filters_interval": "Filters update interval",
|
||||||
"disabled": "Disabled"
|
"disabled": "Disabled",
|
||||||
|
"username_label": "Username",
|
||||||
|
"username_placeholder": "Enter username",
|
||||||
|
"password_label": "Password",
|
||||||
|
"password_placeholder": "Enter password",
|
||||||
|
"sign_in": "Sign in",
|
||||||
|
"logout": "Logout",
|
||||||
|
"forgot_password": "Forgot password?",
|
||||||
|
"forgot_password_desc": "Please follow <0>these steps</0> to create a new password for your user account."
|
||||||
}
|
}
|
||||||
|
|
|
@ -352,10 +352,17 @@
|
||||||
"unblock_all": "Разблокировать все",
|
"unblock_all": "Разблокировать все",
|
||||||
"domain": "Домен",
|
"domain": "Домен",
|
||||||
"answer": "Ответ",
|
"answer": "Ответ",
|
||||||
|
"interval_24_hour": "24 часа",
|
||||||
"interval_hours_0": "{{count}} час",
|
"interval_hours_0": "{{count}} час",
|
||||||
"interval_hours_1": "{{count}} часа",
|
"interval_hours_1": "{{count}} часа",
|
||||||
"interval_hours_2": "{{count}} часов",
|
"interval_hours_2": "{{count}} часов",
|
||||||
"interval_days_0": "{{count}} день",
|
"interval_days_0": "{{count}} день",
|
||||||
"interval_days_1": "{{count}} дня",
|
"interval_days_1": "{{count}} дня",
|
||||||
"interval_days_2": "{{count}} дней"
|
"interval_days_2": "{{count}} дней",
|
||||||
|
"for_last_days_0": "за последний {{count}} день",
|
||||||
|
"for_last_days_1": "за последние {{count}} дня",
|
||||||
|
"for_last_days_2": "за последние {{count}} дней",
|
||||||
|
"number_of_dns_query_days_0": "Количество DNS-запросов за {{count}} день",
|
||||||
|
"number_of_dns_query_days_1": "Количество DNS-запросов за {{count}} дня",
|
||||||
|
"number_of_dns_query_days_2": "Количество DNS-запросов за {{count}} дней"
|
||||||
}
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
|
||||||
|
import { addErrorToast } from './index';
|
||||||
|
import apiClient from '../api/Api';
|
||||||
|
|
||||||
|
export const processLoginRequest = createAction('PROCESS_LOGIN_REQUEST');
|
||||||
|
export const processLoginFailure = createAction('PROCESS_LOGIN_FAILURE');
|
||||||
|
export const processLoginSuccess = createAction('PROCESS_LOGIN_SUCCESS');
|
||||||
|
|
||||||
|
export const processLogin = values => async (dispatch) => {
|
||||||
|
dispatch(processLoginRequest());
|
||||||
|
try {
|
||||||
|
await apiClient.login(values);
|
||||||
|
window.location.replace(window.location.origin);
|
||||||
|
dispatch(processLoginSuccess());
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(processLoginFailure());
|
||||||
|
}
|
||||||
|
};
|
|
@ -510,6 +510,18 @@ class Api {
|
||||||
const { path, method } = this.QUERY_LOG_CLEAR;
|
const { path, method } = this.QUERY_LOG_CLEAR;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login
|
||||||
|
LOGIN = { path: 'login', method: 'POST' };
|
||||||
|
|
||||||
|
login(data) {
|
||||||
|
const { path, method } = this.LOGIN;
|
||||||
|
const config = {
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiClient = new Api();
|
const apiClient = new Api();
|
||||||
|
|
|
@ -64,7 +64,7 @@ class App extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, encryption } = this.props;
|
const { dashboard, encryption, getVersion } = this.props;
|
||||||
const updateAvailable = dashboard.isCoreRunning && dashboard.isUpdateAvailable;
|
const updateAvailable = dashboard.isCoreRunning && dashboard.isUpdateAvailable;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -109,7 +109,12 @@ class App extends Component {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer
|
||||||
|
dnsVersion={dashboard.dnsVersion}
|
||||||
|
dnsPort={dashboard.dnsPort}
|
||||||
|
processingVersion={dashboard.processingVersion}
|
||||||
|
getVersion={getVersion}
|
||||||
|
/>
|
||||||
<Toasts />
|
<Toasts />
|
||||||
<Icons />
|
<Icons />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -127,6 +132,7 @@ App.propTypes = {
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
changeLanguage: PropTypes.func,
|
changeLanguage: PropTypes.func,
|
||||||
encryption: PropTypes.object,
|
encryption: PropTypes.object,
|
||||||
|
getVersion: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNamespaces()(App);
|
export default withNamespaces()(App);
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
.nav-tabs .nav-link {
|
.nav-tabs .nav-link {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -68,42 +69,8 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-version {
|
|
||||||
padding: 7px 0;
|
|
||||||
font-size: 0.80rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-version__value {
|
|
||||||
max-width: 110px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 992px) {
|
|
||||||
.nav-version__value {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-version__link {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
border-bottom: 1px dashed #495057;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-version__text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-brand-img {
|
.header-brand-img {
|
||||||
height: 32px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-item.show .nav-link {
|
.nav-tabs .nav-item.show .nav-link {
|
||||||
|
@ -112,6 +79,56 @@
|
||||||
border-bottom-color: #66b574;
|
border-bottom-color: #66b574;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__logout {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
min-width: 25px;
|
||||||
|
padding: 2px;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #9aa0ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__logout:hover,
|
||||||
|
.header__logout:focus {
|
||||||
|
color: #6e7687;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__logout-icon {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__column:last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 992px) {
|
@media screen and (min-width: 992px) {
|
||||||
.header {
|
.header {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -139,13 +156,31 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-version {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-brand-img {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__logout {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__row {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__column:last-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
margin: 0 -0.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1280px) {
|
@media screen and (min-width: 1280px) {
|
||||||
|
@ -153,10 +188,6 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-version {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ class Menu extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const menuClass = classnames({
|
const menuClass = classnames({
|
||||||
'col-lg-6 mobile-menu': true,
|
'header__column mobile-menu': true,
|
||||||
'mobile-menu--active': this.props.isMenuOpen,
|
'mobile-menu--active': this.props.isMenuOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import classnames from 'classnames';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
import Menu from './Menu';
|
import Menu from './Menu';
|
||||||
import Version from './Version';
|
|
||||||
import logo from '../ui/svg/logo.svg';
|
import logo from '../ui/svg/logo.svg';
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ class Header extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, getVersion, location } = this.props;
|
const { dashboard, location } = this.props;
|
||||||
const { isMenuOpen } = this.state;
|
const { isMenuOpen } = this.state;
|
||||||
const badgeClass = classnames({
|
const badgeClass = classnames({
|
||||||
'badge dns-status': true,
|
'badge dns-status': true,
|
||||||
|
@ -33,21 +32,24 @@ class Header extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="container">
|
<div className="header__container">
|
||||||
<div className="row align-items-center">
|
<div className="header__row">
|
||||||
<div className="header-toggler d-lg-none ml-2 ml-lg-0 collapsed" onClick={this.toggleMenuOpen}>
|
<div
|
||||||
|
className="header-toggler d-lg-none ml-lg-0 collapsed"
|
||||||
|
onClick={this.toggleMenuOpen}
|
||||||
|
>
|
||||||
<span className="header-toggler-icon"></span>
|
<span className="header-toggler-icon"></span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col col-lg-3">
|
<div className="header__column">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Link to="/" className="nav-link pl-0 pr-1">
|
<Link to="/" className="nav-link pl-0 pr-1">
|
||||||
<img src={logo} alt="" className="header-brand-img" />
|
<img src={logo} alt="" className="header-brand-img" />
|
||||||
</Link>
|
</Link>
|
||||||
{!dashboard.proccessing && dashboard.isCoreRunning &&
|
{!dashboard.proccessing && dashboard.isCoreRunning && (
|
||||||
<span className={badgeClass}>
|
<span className={badgeClass}>
|
||||||
<Trans>{dashboard.protectionEnabled ? 'on' : 'off'}</Trans>
|
<Trans>{dashboard.protectionEnabled ? 'on' : 'off'}</Trans>
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<Menu
|
||||||
|
@ -56,14 +58,13 @@ class Header extends Component {
|
||||||
toggleMenuOpen={this.toggleMenuOpen}
|
toggleMenuOpen={this.toggleMenuOpen}
|
||||||
closeMenu={this.closeMenu}
|
closeMenu={this.closeMenu}
|
||||||
/>
|
/>
|
||||||
{!dashboard.processing &&
|
<div className="header__column">
|
||||||
<div className="col col-sm-6 col-lg-3">
|
<div className="header__right">
|
||||||
<Version
|
<a href="/control/logout" className="btn btn-sm btn-outline-secondary">
|
||||||
{ ...dashboard }
|
<Trans>logout</Trans>
|
||||||
getVersion={getVersion}
|
</a>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,6 +76,7 @@ Header.propTypes = {
|
||||||
dashboard: PropTypes.object.isRequired,
|
dashboard: PropTypes.object.isRequired,
|
||||||
location: PropTypes.object.isRequired,
|
location: PropTypes.object.isRequired,
|
||||||
getVersion: PropTypes.func.isRequired,
|
getVersion: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNamespaces()(Header);
|
export default withNamespaces()(Header);
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
.footer {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.footer__row {
|
.footer__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -8,6 +12,12 @@
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer__column--links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.footer__column--language {
|
.footer__column--language {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -16,7 +26,7 @@
|
||||||
.footer__link {
|
.footer__link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 15px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer__link--report {
|
.footer__link--report {
|
||||||
|
@ -42,4 +52,12 @@
|
||||||
min-width: initial;
|
min-width: initial;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer__column--links {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__link {
|
||||||
|
margin: 0 20px 0 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
import { REPOSITORY, LANGUAGES, PRIVACY_POLICY_LINK } from '../../helpers/constants';
|
import { REPOSITORY, LANGUAGES, PRIVACY_POLICY_LINK } from '../../helpers/constants';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
|
|
||||||
|
import Version from './Version';
|
||||||
import './Footer.css';
|
import './Footer.css';
|
||||||
import './Select.css';
|
import './Select.css';
|
||||||
|
|
||||||
|
@ -14,42 +16,98 @@ class Footer extends Component {
|
||||||
|
|
||||||
changeLanguage = (event) => {
|
changeLanguage = (event) => {
|
||||||
i18n.changeLanguage(event.target.value);
|
i18n.changeLanguage(event.target.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
dnsVersion, processingVersion, getVersion,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="footer">
|
<Fragment>
|
||||||
<div className="container">
|
<footer className="footer">
|
||||||
<div className="footer__row">
|
<div className="container">
|
||||||
<div className="footer__column">
|
<div className="footer__row">
|
||||||
<div className="footer__copyright">
|
{!dnsVersion && (
|
||||||
<Trans>copyright</Trans> © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
|
<div className="footer__column">
|
||||||
|
<div className="footer__copyright">
|
||||||
|
<Trans>copyright</Trans> © {this.getYear()}{' '}
|
||||||
|
<a href="https://adguard.com/">AdGuard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="footer__column footer__column--links">
|
||||||
|
<a
|
||||||
|
href={REPOSITORY.URL}
|
||||||
|
className="footer__link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Trans>homepage</Trans>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={PRIVACY_POLICY_LINK}
|
||||||
|
className="footer__link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Trans>privacy_policy</Trans>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={REPOSITORY.ISSUES}
|
||||||
|
className="btn btn-outline-primary btn-sm footer__link footer__link--report"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Trans>report_an_issue</Trans>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="footer__column footer__column--language">
|
||||||
|
<select
|
||||||
|
className="form-control select select--language"
|
||||||
|
value={i18n.language}
|
||||||
|
onChange={this.changeLanguage}
|
||||||
|
>
|
||||||
|
{LANGUAGES.map(language => (
|
||||||
|
<option key={language.key} value={language.key}>
|
||||||
|
{language.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer__column">
|
</div>
|
||||||
<a href={REPOSITORY.URL} className="footer__link" target="_blank" rel="noopener noreferrer">
|
</footer>
|
||||||
<Trans>homepage</Trans>
|
{dnsVersion && (
|
||||||
</a>
|
<div className="footer">
|
||||||
<a href={PRIVACY_POLICY_LINK} className="footer__link" target="_blank" rel="noopener noreferrer">
|
<div className="container">
|
||||||
<Trans>privacy_policy</Trans>
|
<div className="footer__row">
|
||||||
</a>
|
<div className="footer__column">
|
||||||
<a href={REPOSITORY.ISSUES} className="btn btn-outline-primary btn-sm footer__link footer__link--report" target="_blank" rel="noopener noreferrer">
|
<div className="footer__copyright">
|
||||||
<Trans>report_an_issue</Trans>
|
<Trans>copyright</Trans> © {this.getYear()}{' '}
|
||||||
</a>
|
<a href="https://adguard.com/">AdGuard</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer__column footer__column--language">
|
</div>
|
||||||
<select className="form-control select select--language" value={i18n.language} onChange={this.changeLanguage}>
|
<div className="footer__column footer__column--language">
|
||||||
{LANGUAGES.map(language =>
|
<Version
|
||||||
<option key={language.key} value={language.key}>
|
dnsVersion={dnsVersion}
|
||||||
{language.name}
|
processingVersion={processingVersion}
|
||||||
</option>)}
|
getVersion={getVersion}
|
||||||
</select>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</footer>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Footer.propTypes = {
|
||||||
|
dnsVersion: PropTypes.string,
|
||||||
|
processingVersion: PropTypes.bool,
|
||||||
|
getVersion: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
export default withNamespaces()(Footer);
|
export default withNamespaces()(Footer);
|
||||||
|
|
|
@ -131,6 +131,10 @@ const Icons = () => (
|
||||||
<symbol id="question" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
<symbol id="question" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||||
<circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><line x1="12" y1="17" x2="12" y2="17" />
|
<circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><line x1="12" y1="17" x2="12" y2="17" />
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="logout" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line>
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title__actions {
|
.page-title__actions {
|
||||||
display: inline-block;
|
display: block;
|
||||||
vertical-align: baseline;
|
}
|
||||||
margin-left: 20px;
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.page-title__actions {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: baseline;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ section {
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
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-size: 0.9375rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,15 +2,17 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
|
import './Version.css';
|
||||||
|
|
||||||
const Version = (props) => {
|
const Version = (props) => {
|
||||||
const {
|
const {
|
||||||
dnsVersion, dnsAddresses, processingVersion, t,
|
dnsVersion, processingVersion, t,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="nav-version">
|
<div className="version">
|
||||||
<div className="nav-version__text">
|
<div className="version__text">
|
||||||
<Trans>version</Trans>: <span className="nav-version__value" title={dnsVersion}>{dnsVersion}</span>
|
<Trans>version</Trans>: <span className="version__value" title={dnsVersion}>{dnsVersion}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
|
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
|
||||||
|
@ -23,24 +25,12 @@ const Version = (props) => {
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="nav-version__link">
|
|
||||||
<div className="popover__trigger popover__trigger--address">
|
|
||||||
<Trans>dns_addresses</Trans>
|
|
||||||
</div>
|
|
||||||
<div className="popover__body popover__body--address">
|
|
||||||
<div className="popover__list">
|
|
||||||
{dnsAddresses.map(ip => <li key={ip}>{ip}</li>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Version.propTypes = {
|
Version.propTypes = {
|
||||||
dnsVersion: PropTypes.string.isRequired,
|
dnsVersion: PropTypes.string.isRequired,
|
||||||
dnsAddresses: PropTypes.array.isRequired,
|
|
||||||
dnsPort: PropTypes.number.isRequired,
|
|
||||||
getVersion: PropTypes.func.isRequired,
|
getVersion: PropTypes.func.isRequired,
|
||||||
processingVersion: PropTypes.bool.isRequired,
|
processingVersion: PropTypes.bool.isRequired,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as actionCreators from '../actions';
|
import { getVersion } from '../actions';
|
||||||
import Header from '../components/Header';
|
import Header from '../components/Header';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
|
@ -8,7 +8,11 @@ const mapStateToProps = (state) => {
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
getVersion,
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
actionCreators,
|
mapDispatchToProps,
|
||||||
)(Header);
|
)(Header);
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const renderField = ({
|
||||||
placeholder,
|
placeholder,
|
||||||
type,
|
type,
|
||||||
disabled,
|
disabled,
|
||||||
|
autoComplete,
|
||||||
meta: { touched, error },
|
meta: { touched, error },
|
||||||
}) => (
|
}) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -20,6 +21,7 @@ export const renderField = ({
|
||||||
type={type}
|
type={type}
|
||||||
className={className}
|
className={className}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
autoComplete={autoComplete}
|
||||||
/>
|
/>
|
||||||
{!disabled &&
|
{!disabled &&
|
||||||
touched &&
|
touched &&
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.setup {
|
.setup {
|
||||||
min-height: calc(100vh - 80px);
|
min-height: calc(100vh - 71px);
|
||||||
line-height: 1.48;
|
line-height: 1.48;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 onSubmit={handleSubmit} className="card">
|
||||||
|
<div className="card-body p-6">
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<label className="form__label" htmlFor="username">
|
||||||
|
<Trans>username_label</Trans>
|
||||||
|
</label>
|
||||||
|
<Field
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
component={renderField}
|
||||||
|
placeholder={t('username_placeholder')}
|
||||||
|
autoComplete="username"
|
||||||
|
disabled={processing}
|
||||||
|
validate={[required]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<label className="form__label" htmlFor="password">
|
||||||
|
<Trans>password_label</Trans>
|
||||||
|
</label>
|
||||||
|
<Field
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
component={renderField}
|
||||||
|
placeholder={t('password_placeholder')}
|
||||||
|
autoComplete="current-password"
|
||||||
|
disabled={processing}
|
||||||
|
validate={[required]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-footer">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-block"
|
||||||
|
disabled={processing || invalid}
|
||||||
|
>
|
||||||
|
<Trans>sign_in</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<div className="login">
|
||||||
|
<div className="login__form">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<img src={logo} className="h-6" alt="logo" />
|
||||||
|
</div>
|
||||||
|
<Form onSubmit={this.handleSubmit} processing={processingLogin} />
|
||||||
|
<div className="login__info">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-link login__link"
|
||||||
|
onClick={this.toggleText}
|
||||||
|
>
|
||||||
|
<Trans>forgot_password</Trans>
|
||||||
|
</button>
|
||||||
|
{isForgotPasswordVisible && (
|
||||||
|
<div className="login__message">
|
||||||
|
<Trans
|
||||||
|
components={[
|
||||||
|
<a
|
||||||
|
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#password-reset"
|
||||||
|
key="0"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
link
|
||||||
|
</a>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
forgot_password_desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
<Toasts />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Login.propTypes = {
|
||||||
|
login: PropTypes.object.isRequired,
|
||||||
|
processLogin: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = ({ login, toasts }) => ({ login, toasts });
|
||||||
|
|
||||||
|
export default flow([
|
||||||
|
withNamespaces(),
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
actionCreators,
|
||||||
|
),
|
||||||
|
])(Login);
|
|
@ -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(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Login />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
|
@ -79,8 +79,8 @@ const dashboard = handleActions(
|
||||||
dnsVersion: version,
|
dnsVersion: version,
|
||||||
dnsPort,
|
dnsPort,
|
||||||
dnsAddresses,
|
dnsAddresses,
|
||||||
upstreamDns: upstreamDns.join('\n'),
|
upstreamDns: (upstreamDns && upstreamDns.join('\n')) || '',
|
||||||
bootstrapDns: bootstrapDns.join('\n'),
|
bootstrapDns: (bootstrapDns && bootstrapDns.join('\n')) || '',
|
||||||
allServers,
|
allServers,
|
||||||
protectionEnabled,
|
protectionEnabled,
|
||||||
language,
|
language,
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
|
@ -10,8 +10,10 @@ const CopyPlugin = require('copy-webpack-plugin');
|
||||||
const RESOURCES_PATH = path.resolve(__dirname);
|
const RESOURCES_PATH = path.resolve(__dirname);
|
||||||
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
|
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
|
||||||
const ENTRY_INSTALL = path.resolve(RESOURCES_PATH, 'src/install/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_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
|
||||||
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.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 FAVICON_PATH = path.resolve(RESOURCES_PATH, 'public/favicon.png');
|
||||||
|
|
||||||
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
|
const PUBLIC_PATH = path.resolve(__dirname, '../build/static');
|
||||||
|
@ -22,6 +24,7 @@ const config = {
|
||||||
entry: {
|
entry: {
|
||||||
main: ENTRY_REACT,
|
main: ENTRY_REACT,
|
||||||
install: ENTRY_INSTALL,
|
install: ENTRY_INSTALL,
|
||||||
|
login: ENTRY_LOGIN,
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: PUBLIC_PATH,
|
path: PUBLIC_PATH,
|
||||||
|
@ -116,6 +119,13 @@ const config = {
|
||||||
filename: 'install.html',
|
filename: 'install.html',
|
||||||
template: HTML_INSTALL_PATH,
|
template: HTML_INSTALL_PATH,
|
||||||
}),
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
inject: true,
|
||||||
|
cache: false,
|
||||||
|
chunks: ['login'],
|
||||||
|
filename: 'login.html',
|
||||||
|
template: HTML_LOGIN_PATH,
|
||||||
|
}),
|
||||||
new ExtractTextPlugin({
|
new ExtractTextPlugin({
|
||||||
filename: '[name].[contenthash].css',
|
filename: '[name].[contenthash].css',
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in New Issue