+ client: split settings page into several pages
This commit is contained in:
parent
1d09ff0562
commit
cf53653cfa
|
@ -96,6 +96,10 @@
|
|||
"no_servers_specified": "No servers specified",
|
||||
"no_settings": "No settings",
|
||||
"general_settings": "General settings",
|
||||
"dns_settings": "DNS settings",
|
||||
"encryption_settings": "Encryption settings",
|
||||
"dhcp_settings": "DHCP settings",
|
||||
"clients_settings": "Clients settings",
|
||||
"upstream_dns": "Upstream DNS servers",
|
||||
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> as an upstream.",
|
||||
"test_upstream_btn": "Test upstreams",
|
||||
|
|
|
@ -13,6 +13,12 @@ import Header from '../../containers/Header';
|
|||
import Dashboard from '../../containers/Dashboard';
|
||||
import Settings from '../../containers/Settings';
|
||||
import Filters from '../../containers/Filters';
|
||||
|
||||
import Dns from '../../containers/Dns';
|
||||
import Encryption from '../../containers/Encryption';
|
||||
import Dhcp from '../../containers/Dhcp';
|
||||
import Clients from '../../containers/Clients';
|
||||
|
||||
import Logs from '../../containers/Logs';
|
||||
import SetupGuide from '../../containers/SetupGuide';
|
||||
import Toasts from '../Toasts';
|
||||
|
@ -41,7 +47,7 @@ class App extends Component {
|
|||
|
||||
handleUpdate = () => {
|
||||
this.props.getUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
setLanguage = () => {
|
||||
const { processing, language } = this.props.dashboard;
|
||||
|
@ -55,19 +61,17 @@ class App extends Component {
|
|||
i18n.on('languageChanged', (lang) => {
|
||||
this.props.changeLanguage(lang);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, encryption } = this.props;
|
||||
const updateAvailable =
|
||||
!dashboard.processingVersions &&
|
||||
dashboard.isCoreRunning &&
|
||||
dashboard.isUpdateAvailable;
|
||||
!dashboard.processingVersions && dashboard.isCoreRunning && dashboard.isUpdateAvailable;
|
||||
|
||||
return (
|
||||
<HashRouter hashType='noslash'>
|
||||
<HashRouter hashType="noslash">
|
||||
<Fragment>
|
||||
{updateAvailable &&
|
||||
{updateAvailable && (
|
||||
<Fragment>
|
||||
<UpdateTopline
|
||||
url={dashboard.announcementUrl}
|
||||
|
@ -78,29 +82,33 @@ class App extends Component {
|
|||
/>
|
||||
<UpdateOverlay processingUpdate={dashboard.processingUpdate} />
|
||||
</Fragment>
|
||||
}
|
||||
{!encryption.processing &&
|
||||
)}
|
||||
{!encryption.processing && (
|
||||
<EncryptionTopline notAfter={encryption.not_after} />
|
||||
}
|
||||
)}
|
||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||
<Route component={Header} />
|
||||
<div className="container container--wrap">
|
||||
{!dashboard.processing && !dashboard.isCoreRunning &&
|
||||
{!dashboard.processing && !dashboard.isCoreRunning && (
|
||||
<div className="row row-cards">
|
||||
<div className="col-lg-12">
|
||||
<Status handleStatusChange={this.handleStatusChange} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{!dashboard.processing && dashboard.isCoreRunning &&
|
||||
)}
|
||||
{!dashboard.processing && dashboard.isCoreRunning && (
|
||||
<Fragment>
|
||||
<Route path="/" exact component={Dashboard} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/dns" component={Dns} />
|
||||
<Route path="/encryption" component={Encryption} />
|
||||
<Route path="/dhcp" component={Dhcp} />
|
||||
<Route path="/clients" component={Clients} />
|
||||
<Route path="/filters" component={Filters} />
|
||||
<Route path="/logs" component={Logs} />
|
||||
<Route path="/guide" component={SetupGuide} />
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
<Toasts />
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
stroke: #9aa0ac;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active .nav-icon {
|
||||
.nav-tabs .nav-link.active .nav-icon,
|
||||
.nav-tabs .nav-item.show .nav-icon {
|
||||
stroke: #66b574;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active:hover .nav-icon {
|
||||
.nav-tabs .nav-link.active:hover .nav-icon,
|
||||
.nav-tabs .nav-item.show:hover .nav-icon {
|
||||
stroke: #58a273;
|
||||
}
|
||||
|
||||
|
@ -87,6 +89,12 @@
|
|||
height: 32px;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item.show .nav-link {
|
||||
color: #66b574;
|
||||
background-color: #fff;
|
||||
border-bottom-color: #66b574;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 992px) {
|
||||
.header {
|
||||
padding: 0;
|
||||
|
|
|
@ -5,6 +5,9 @@ import enhanceWithClickOutside from 'react-click-outside';
|
|||
import classnames from 'classnames';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
|
||||
import { SETTINGS_URLS } from '../../helpers/constants';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
|
||||
class Menu extends Component {
|
||||
handleClickOutside = () => {
|
||||
this.props.closeMenu();
|
||||
|
@ -14,49 +17,86 @@ class Menu extends Component {
|
|||
this.props.toggleMenuOpen();
|
||||
};
|
||||
|
||||
getActiveClassForSettings = () => {
|
||||
const { pathname } = this.props.location;
|
||||
const isSettingsPage = SETTINGS_URLS.some(item => item === pathname);
|
||||
|
||||
return isSettingsPage ? 'active' : '';
|
||||
};
|
||||
|
||||
render() {
|
||||
const menuClass = classnames({
|
||||
'col-lg-6 mobile-menu': true,
|
||||
'mobile-menu--active': this.props.isMenuOpen,
|
||||
});
|
||||
|
||||
const dropdownControlClass = `nav-link ${this.getActiveClassForSettings()}`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={menuClass}>
|
||||
<ul className="nav nav-tabs border-0 flex-column flex-lg-row flex-nowrap">
|
||||
<li className="nav-item border-bottom d-lg-none" onClick={this.toggleMenu}>
|
||||
<div className="nav-link nav-link--back">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#back" />
|
||||
</svg>
|
||||
<Trans>back</Trans>
|
||||
</div>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/" exact={true} className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" stroke="#9aa0ac" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#dashboard" />
|
||||
</svg>
|
||||
<Trans>dashboard</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/settings" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/></svg>
|
||||
<Trans>settings</Trans>
|
||||
<Dropdown
|
||||
label={this.props.t('settings')}
|
||||
baseClassName="dropdown nav-item"
|
||||
controlClassName={dropdownControlClass}
|
||||
icon="settings"
|
||||
>
|
||||
<Fragment>
|
||||
<NavLink to="/settings" className="dropdown-item">
|
||||
<Trans>general_settings</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<NavLink to="/dns" className="dropdown-item">
|
||||
<Trans>dns_settings</Trans>
|
||||
</NavLink>
|
||||
<NavLink to="/encryption" className="dropdown-item">
|
||||
<Trans>encryption_settings</Trans>
|
||||
</NavLink>
|
||||
<NavLink to="/clients" className="dropdown-item">
|
||||
<Trans>clients_settings</Trans>
|
||||
</NavLink>
|
||||
<NavLink to="/dhcp" className="dropdown-item">
|
||||
<Trans>dhcp_settings</Trans>
|
||||
</NavLink>
|
||||
</Fragment>
|
||||
</Dropdown>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/filters" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#filters" />
|
||||
</svg>
|
||||
<Trans>filters</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/logs" className="nav-link">
|
||||
<svg className="nav-icon" fill="none" height="24" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/></svg>
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#log" />
|
||||
</svg>
|
||||
<Trans>query_log</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/guide" href="/guide" className="nav-link">
|
||||
<svg className="nav-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#66b574" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
|
||||
<NavLink to="/guide" className="nav-link">
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref="#setup" />
|
||||
</svg>
|
||||
<Trans>setup_guide</Trans>
|
||||
</NavLink>
|
||||
</li>
|
||||
|
@ -71,6 +111,8 @@ Menu.propTypes = {
|
|||
isMenuOpen: PropTypes.bool,
|
||||
closeMenu: PropTypes.func,
|
||||
toggleMenuOpen: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export default withNamespaces()(enhanceWithClickOutside(Menu));
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
|
||||
import Card from '../../ui/Card';
|
||||
import Modal from './Modal';
|
||||
|
||||
class ClientsTable extends Component {
|
||||
handleFormAdd = (values) => {
|
||||
this.props.addClient(values);
|
||||
};
|
||||
|
||||
handleFormUpdate = (values, name) => {
|
||||
this.props.updateClient(values, name);
|
||||
};
|
||||
|
||||
handleSubmit = (values) => {
|
||||
if (this.props.modalType === MODAL_TYPE.EDIT) {
|
||||
this.handleFormUpdate(values, this.props.modalClientName);
|
||||
} else {
|
||||
this.handleFormAdd(values);
|
||||
}
|
||||
};
|
||||
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
getClient = (name, clients) => {
|
||||
const client = clients.find(item => name === item.name);
|
||||
|
||||
if (client) {
|
||||
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
|
||||
|
||||
return {
|
||||
identifier,
|
||||
use_global_settings: true,
|
||||
...client,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: CLIENT_ID.IP,
|
||||
use_global_settings: true,
|
||||
};
|
||||
};
|
||||
|
||||
getStats = (ip, stats) => {
|
||||
if (stats && stats.top_clients) {
|
||||
return stats.top_clients[ip];
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
handleDelete = (data) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(this.props.t('client_confirm_delete', { key: data.name }))) {
|
||||
this.props.deleteClient(data);
|
||||
}
|
||||
};
|
||||
|
||||
columns = [
|
||||
{
|
||||
Header: this.props.t('table_client'),
|
||||
accessor: 'ip',
|
||||
Cell: (row) => {
|
||||
if (row.original && row.original.mac) {
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={row.original.mac}>
|
||||
{row.original.mac} <em>(MAC)</em>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else if (row.value) {
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={row.value}>
|
||||
{row.value} <em>(IP)</em>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_name'),
|
||||
accessor: 'name',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: this.props.t('settings'),
|
||||
accessor: 'use_global_settings',
|
||||
Cell: ({ value }) => {
|
||||
const title = value ? (
|
||||
<Trans>settings_global</Trans>
|
||||
) : (
|
||||
<Trans>settings_custom</Trans>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__text" title={title}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_statistics'),
|
||||
accessor: 'statistics',
|
||||
Cell: (row) => {
|
||||
const clientIP = row.original.ip;
|
||||
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
|
||||
|
||||
if (clientStats) {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={clientStats}>
|
||||
{clientStats}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '–';
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('actions_table_header'),
|
||||
accessor: 'actions',
|
||||
maxWidth: 150,
|
||||
Cell: (row) => {
|
||||
const clientName = row.original.name;
|
||||
const {
|
||||
toggleClientModal, processingDeleting, processingUpdating, t,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm mr-2"
|
||||
onClick={() =>
|
||||
toggleClientModal({
|
||||
type: MODAL_TYPE.EDIT,
|
||||
name: clientName,
|
||||
})
|
||||
}
|
||||
disabled={processingUpdating}
|
||||
title={t('edit_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
onClick={() => this.handleDelete({ name: clientName })}
|
||||
disabled={processingDeleting}
|
||||
title={t('delete_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
clients,
|
||||
isModalOpen,
|
||||
modalType,
|
||||
modalClientName,
|
||||
toggleClientModal,
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
} = this.props;
|
||||
|
||||
const currentClientData = this.getClient(modalClientName, clients);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('clients_title')}
|
||||
subtitle={t('clients_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Fragment>
|
||||
<ReactTable
|
||||
data={clients || []}
|
||||
columns={this.columns}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination={true}
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
ofText={t('of_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={t('clients_not_found')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard mt-3"
|
||||
onClick={() => toggleClientModal(MODAL_TYPE.ADD)}
|
||||
disabled={processingAdding}
|
||||
>
|
||||
<Trans>client_add</Trans>
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
isModalOpen={isModalOpen}
|
||||
modalType={modalType}
|
||||
toggleClientModal={toggleClientModal}
|
||||
currentClientData={currentClientData}
|
||||
handleSubmit={this.handleSubmit}
|
||||
processingAdding={processingAdding}
|
||||
processingUpdating={processingUpdating}
|
||||
/>
|
||||
</Fragment>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClientsTable.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
clients: PropTypes.array.isRequired,
|
||||
topStats: PropTypes.object.isRequired,
|
||||
toggleClientModal: PropTypes.func.isRequired,
|
||||
deleteClient: PropTypes.func.isRequired,
|
||||
addClient: PropTypes.func.isRequired,
|
||||
updateClient: PropTypes.func.isRequired,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
modalType: PropTypes.string.isRequired,
|
||||
modalClientName: PropTypes.string.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
processingDeleting: PropTypes.bool.isRequired,
|
||||
processingUpdating: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(ClientsTable);
|
|
@ -1,251 +1,50 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withNamespaces } from 'react-i18next';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
|
||||
import Card from '../../ui/Card';
|
||||
import Modal from './Modal';
|
||||
import ClientsTable from './ClientsTable';
|
||||
import AutoClients from './AutoClients';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
|
||||
class Clients extends Component {
|
||||
handleFormAdd = (values) => {
|
||||
this.props.addClient(values);
|
||||
};
|
||||
|
||||
handleFormUpdate = (values, name) => {
|
||||
this.props.updateClient(values, name);
|
||||
};
|
||||
|
||||
handleSubmit = (values) => {
|
||||
if (this.props.modalType === MODAL_TYPE.EDIT) {
|
||||
this.handleFormUpdate(values, this.props.modalClientName);
|
||||
} else {
|
||||
this.handleFormAdd(values);
|
||||
}
|
||||
};
|
||||
|
||||
cellWrap = ({ value }) => (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
getClient = (name, clients) => {
|
||||
const client = clients.find(item => name === item.name);
|
||||
|
||||
if (client) {
|
||||
const identifier = client.mac ? CLIENT_ID.MAC : CLIENT_ID.IP;
|
||||
|
||||
return {
|
||||
identifier,
|
||||
use_global_settings: true,
|
||||
...client,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: CLIENT_ID.IP,
|
||||
use_global_settings: true,
|
||||
};
|
||||
};
|
||||
|
||||
getStats = (ip, stats) => {
|
||||
if (stats && stats.top_clients) {
|
||||
return stats.top_clients[ip];
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
handleDelete = (data) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm(this.props.t('client_confirm_delete', { key: data.name }))) {
|
||||
this.props.deleteClient(data);
|
||||
}
|
||||
}
|
||||
|
||||
columns = [
|
||||
{
|
||||
Header: this.props.t('table_client'),
|
||||
accessor: 'ip',
|
||||
Cell: (row) => {
|
||||
if (row.original && row.original.mac) {
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={row.original.mac}>
|
||||
{row.original.mac} <em>(MAC)</em>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else if (row.value) {
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<span className="logs__text" title={row.value}>
|
||||
{row.value} <em>(IP)</em>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_name'),
|
||||
accessor: 'name',
|
||||
Cell: this.cellWrap,
|
||||
},
|
||||
{
|
||||
Header: this.props.t('settings'),
|
||||
accessor: 'use_global_settings',
|
||||
Cell: ({ value }) => {
|
||||
const title = value ? (
|
||||
<Trans>settings_global</Trans>
|
||||
) : (
|
||||
<Trans>settings_custom</Trans>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--overflow">
|
||||
<div className="logs__text" title={title}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('table_statistics'),
|
||||
accessor: 'statistics',
|
||||
Cell: (row) => {
|
||||
const clientIP = row.original.ip;
|
||||
const clientStats = clientIP && this.getStats(clientIP, this.props.topStats);
|
||||
|
||||
if (clientStats) {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={clientStats}>
|
||||
{clientStats}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '–';
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: this.props.t('actions_table_header'),
|
||||
accessor: 'actions',
|
||||
maxWidth: 150,
|
||||
Cell: (row) => {
|
||||
const clientName = row.original.name;
|
||||
const {
|
||||
toggleClientModal,
|
||||
processingDeleting,
|
||||
processingUpdating,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="logs__row logs__row--center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm mr-2"
|
||||
onClick={() =>
|
||||
toggleClientModal({
|
||||
type: MODAL_TYPE.EDIT,
|
||||
name: clientName,
|
||||
})
|
||||
}
|
||||
disabled={processingUpdating}
|
||||
title={t('edit_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||
onClick={() => this.handleDelete({ name: clientName })}
|
||||
disabled={processingDeleting}
|
||||
title={t('delete_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
clients,
|
||||
isModalOpen,
|
||||
modalType,
|
||||
modalClientName,
|
||||
toggleClientModal,
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
} = this.props;
|
||||
|
||||
const currentClientData = this.getClient(modalClientName, clients);
|
||||
const { dashboard, clients, t } = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t('clients_title')}
|
||||
subtitle={t('clients_desc')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Fragment>
|
||||
<ReactTable
|
||||
data={clients || []}
|
||||
columns={this.columns}
|
||||
className="-striped -highlight card-table-overflow"
|
||||
showPagination={true}
|
||||
defaultPageSize={10}
|
||||
minRows={5}
|
||||
previousText={t('previous_btn')}
|
||||
nextText={t('next_btn')}
|
||||
loadingText={t('loading_table_status')}
|
||||
pageText={t('page_table_footer_text')}
|
||||
ofText={t('of_table_footer_text')}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={t('clients_not_found')}
|
||||
<PageTitle title={t('clients_settings')} />
|
||||
{!dashboard.processingTopStats || (!dashboard.processingClients && <Loading />)}
|
||||
{!dashboard.processingTopStats && !dashboard.processingClients && (
|
||||
<Fragment>
|
||||
<ClientsTable
|
||||
clients={dashboard.clients}
|
||||
topStats={dashboard.topStats}
|
||||
isModalOpen={clients.isModalOpen}
|
||||
modalClientName={clients.modalClientName}
|
||||
modalType={clients.modalType}
|
||||
addClient={this.props.addClient}
|
||||
updateClient={this.props.updateClient}
|
||||
deleteClient={this.props.deleteClient}
|
||||
toggleClientModal={this.props.toggleClientModal}
|
||||
processingAdding={clients.processingAdding}
|
||||
processingDeleting={clients.processingDeleting}
|
||||
processingUpdating={clients.processingUpdating}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard mt-3"
|
||||
onClick={() => toggleClientModal(MODAL_TYPE.ADD)}
|
||||
disabled={processingAdding}
|
||||
>
|
||||
<Trans>client_add</Trans>
|
||||
</button>
|
||||
|
||||
<Modal
|
||||
isModalOpen={isModalOpen}
|
||||
modalType={modalType}
|
||||
toggleClientModal={toggleClientModal}
|
||||
currentClientData={currentClientData}
|
||||
handleSubmit={this.handleSubmit}
|
||||
processingAdding={processingAdding}
|
||||
processingUpdating={processingUpdating}
|
||||
<AutoClients
|
||||
autoClients={dashboard.autoClients}
|
||||
topStats={dashboard.topStats}
|
||||
/>
|
||||
</Fragment>
|
||||
</Card>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Clients.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
clients: PropTypes.array.isRequired,
|
||||
topStats: PropTypes.object.isRequired,
|
||||
toggleClientModal: PropTypes.func.isRequired,
|
||||
|
|
|
@ -9,8 +9,15 @@ import Leases from './Leases';
|
|||
import StaticLeases from './StaticLeases/index';
|
||||
import Card from '../../ui/Card';
|
||||
import Accordion from '../../ui/Accordion';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
|
||||
class Dhcp extends Component {
|
||||
componentDidMount() {
|
||||
this.props.getDhcpStatus();
|
||||
this.props.getDhcpInterfaces();
|
||||
}
|
||||
|
||||
handleFormSubmit = (values) => {
|
||||
if (values.interface_name) {
|
||||
this.props.setDhcpConfig(values);
|
||||
|
@ -19,7 +26,7 @@ class Dhcp extends Component {
|
|||
|
||||
handleToggle = (config) => {
|
||||
this.props.toggleDhcp(config);
|
||||
}
|
||||
};
|
||||
|
||||
getToggleDhcpButton = () => {
|
||||
const {
|
||||
|
@ -54,17 +61,13 @@ class Dhcp extends Component {
|
|||
className="btn btn-standard mr-2 btn-success"
|
||||
onClick={() => this.handleToggle(config)}
|
||||
disabled={
|
||||
!filledConfig
|
||||
|| !check
|
||||
|| otherDhcpFound
|
||||
|| processingDhcp
|
||||
|| processingConfig
|
||||
!filledConfig || !check || otherDhcpFound || processingDhcp || processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>dhcp_enable</Trans>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getActiveDhcpMessage = (t, check) => {
|
||||
const { found } = check.otherServer;
|
||||
|
@ -95,7 +98,7 @@ class Dhcp extends Component {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getDhcpWarning = (check) => {
|
||||
if (check.otherServer.found === DHCP_STATUS_RESPONSE.NO) {
|
||||
|
@ -107,7 +110,7 @@ class Dhcp extends Component {
|
|||
<Trans>dhcp_warning</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getStaticIpWarning = (t, check, interfaceName) => {
|
||||
if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) {
|
||||
|
@ -125,17 +128,15 @@ class Dhcp extends Component {
|
|||
</Fragment>
|
||||
);
|
||||
} else if (
|
||||
check.staticIP.static === DHCP_STATUS_RESPONSE.NO
|
||||
&& check.staticIP.ip
|
||||
&& interfaceName
|
||||
check.staticIP.static === DHCP_STATUS_RESPONSE.NO &&
|
||||
check.staticIP.ip &&
|
||||
interfaceName
|
||||
) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="text-secondary mb-2">
|
||||
<Trans
|
||||
components={[
|
||||
<strong key="0">example</strong>,
|
||||
]}
|
||||
components={[<strong key="0">example</strong>]}
|
||||
values={{
|
||||
interfaceName,
|
||||
ipAddress: check.staticIP.ip,
|
||||
|
@ -150,7 +151,7 @@ class Dhcp extends Component {
|
|||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, dhcp } = this.props;
|
||||
|
@ -158,17 +159,20 @@ class Dhcp extends Component {
|
|||
'btn btn-primary btn-standard': true,
|
||||
'btn btn-primary btn-standard btn-loading': dhcp.processingStatus,
|
||||
});
|
||||
const {
|
||||
enabled,
|
||||
interface_name,
|
||||
...values
|
||||
} = dhcp.config;
|
||||
const { enabled, interface_name, ...values } = dhcp.config;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card title={ t('dhcp_title') } subtitle={ t('dhcp_description') } bodyType="card-body box-body--settings">
|
||||
<PageTitle title={t('dhcp_settings')} />
|
||||
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
|
||||
{!dhcp.processing && !dhcp.processingInterfaces && (
|
||||
<Fragment>
|
||||
<Card
|
||||
title={t('dhcp_title')}
|
||||
subtitle={t('dhcp_description')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="dhcp">
|
||||
{!dhcp.processing &&
|
||||
<Fragment>
|
||||
<Form
|
||||
onSubmit={this.handleFormSubmit}
|
||||
|
@ -191,35 +195,38 @@ class Dhcp extends Component {
|
|||
this.props.findActiveDhcp(interface_name)
|
||||
}
|
||||
disabled={
|
||||
enabled
|
||||
|| !interface_name
|
||||
|| dhcp.processingConfig
|
||||
enabled || !interface_name || dhcp.processingConfig
|
||||
}
|
||||
>
|
||||
<Trans>check_dhcp_servers</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{!enabled && dhcp.check &&
|
||||
{!enabled && dhcp.check && (
|
||||
<Fragment>
|
||||
{this.getStaticIpWarning(t, dhcp.check, interface_name)}
|
||||
{this.getActiveDhcpMessage(t, dhcp.check)}
|
||||
{this.getDhcpWarning(dhcp.check)}
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</Card>
|
||||
{!dhcp.processing && dhcp.config.enabled &&
|
||||
{dhcp.config.enabled && (
|
||||
<Fragment>
|
||||
<Card title={ t('dhcp_leases') } bodyType="card-body box-body--settings">
|
||||
<Card
|
||||
title={t('dhcp_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Leases leases={dhcp.leases} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={ t('dhcp_static_leases') } bodyType="card-body box-body--settings">
|
||||
<Card
|
||||
title={t('dhcp_static_leases')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<StaticLeases
|
||||
|
@ -244,7 +251,9 @@ class Dhcp extends Component {
|
|||
</div>
|
||||
</Card>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -256,10 +265,10 @@ Dhcp.propTypes = {
|
|||
getDhcpStatus: PropTypes.func,
|
||||
setDhcpConfig: PropTypes.func,
|
||||
findActiveDhcp: PropTypes.func,
|
||||
handleSubmit: PropTypes.func,
|
||||
addStaticLease: PropTypes.func,
|
||||
removeStaticLease: PropTypes.func,
|
||||
toggleLeaseModal: PropTypes.func,
|
||||
getDhcpInterfaces: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,12 @@ const Examples = props => (
|
|||
<p>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://kb.adguard.com/general/dns-providers" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://kb.adguard.com/general/dns-providers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS providers
|
||||
</a>,
|
||||
]}
|
||||
|
@ -25,7 +30,12 @@ const Examples = props => (
|
|||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://en.wikipedia.org/wiki/DNS_over_TLS" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/DNS_over_TLS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS-over-TLS
|
||||
</a>,
|
||||
]}
|
||||
|
@ -39,7 +49,12 @@ const Examples = props => (
|
|||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS-over-HTTPS
|
||||
</a>,
|
||||
]}
|
||||
|
@ -56,13 +71,28 @@ const Examples = props => (
|
|||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://dnscrypt.info/stamps/" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://dnscrypt.info/stamps/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
DNS Stamps
|
||||
</a>,
|
||||
<a href="https://dnscrypt.info/" target="_blank" rel="noopener noreferrer" key="1">
|
||||
<a
|
||||
href="https://dnscrypt.info/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="1"
|
||||
>
|
||||
DNSCrypt
|
||||
</a>,
|
||||
<a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank" rel="noopener noreferrer" key="2">
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/DNS_over_HTTPS"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="2"
|
||||
>
|
||||
DNS-over-HTTPS
|
||||
</a>,
|
||||
]}
|
||||
|
@ -76,7 +106,12 @@ const Examples = props => (
|
|||
<span>
|
||||
<Trans
|
||||
components={[
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains" target="_blank" rel="noopener noreferrer" key="0">
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
key="0"
|
||||
>
|
||||
Link
|
||||
</a>,
|
||||
]}
|
|
@ -6,7 +6,7 @@ import { Trans, withNamespaces } from 'react-i18next';
|
|||
import flow from 'lodash/flow';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { renderSelectField } from '../../../helpers/form';
|
||||
import { renderSelectField } from '../../../../helpers/form';
|
||||
import Examples from './Examples';
|
||||
|
||||
let Form = (props) => {
|
||||
|
@ -84,11 +84,13 @@ let Form = (props) => {
|
|||
<button
|
||||
type="button"
|
||||
className={testButtonClass}
|
||||
onClick={() => testUpstream({
|
||||
onClick={() =>
|
||||
testUpstream({
|
||||
upstream_dns: upstreamDns,
|
||||
bootstrap_dns: bootstrapDns,
|
||||
all_servers: allServers,
|
||||
})}
|
||||
})
|
||||
}
|
||||
disabled={!upstreamDns || processingTestUpstream}
|
||||
>
|
||||
<Trans>test_upstream_btn</Trans>
|
||||
|
@ -97,10 +99,7 @@ let Form = (props) => {
|
|||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={
|
||||
submitting
|
||||
|| invalid
|
||||
|| processingSetUpstream
|
||||
|| processingTestUpstream
|
||||
submitting || invalid || processingSetUpstream || processingTestUpstream
|
||||
}
|
||||
>
|
||||
<Trans>apply_btn</Trans>
|
||||
|
@ -140,5 +139,7 @@ Form = connect((state) => {
|
|||
|
||||
export default flow([
|
||||
withNamespaces(),
|
||||
reduxForm({ form: 'upstreamForm' }),
|
||||
reduxForm({
|
||||
form: 'upstreamForm',
|
||||
}),
|
||||
])(Form);
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import { withNamespaces } from 'react-i18next';
|
||||
|
||||
import Form from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
import Card from '../../../ui/Card';
|
||||
|
||||
class Upstream extends Component {
|
||||
handleSubmit = (values) => {
|
||||
|
@ -12,7 +12,7 @@ class Upstream extends Component {
|
|||
|
||||
handleTest = (values) => {
|
||||
this.props.testUpstream(values);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
|
@ -0,0 +1,35 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
|
||||
import Upstream from './Upstream';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
|
||||
const Dns = (props) => {
|
||||
const { dashboard, settings, t } = props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={t('dns_settings')} />
|
||||
<Upstream
|
||||
upstreamDns={dashboard.upstreamDns}
|
||||
bootstrapDns={dashboard.bootstrapDns}
|
||||
allServers={dashboard.allServers}
|
||||
setUpstream={props.setUpstream}
|
||||
testUpstream={props.testUpstream}
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
processingSetUpstream={settings.processingSetUpstream}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
Dns.propTypes = {
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
setUpstream: PropTypes.func.isRequired,
|
||||
testUpstream: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withNamespaces()(Dns);
|
|
@ -66,14 +66,15 @@ let Form = (props) => {
|
|||
setTlsConfig,
|
||||
} = props;
|
||||
|
||||
const isSavingDisabled = invalid
|
||||
|| submitting
|
||||
|| processingConfig
|
||||
|| processingValidate
|
||||
|| (isEnabled && (!privateKey || !certificateChain))
|
||||
|| (privateKey && !valid_key)
|
||||
|| (certificateChain && !valid_cert)
|
||||
|| (privateKey && certificateChain && !valid_pair);
|
||||
const isSavingDisabled =
|
||||
invalid ||
|
||||
submitting ||
|
||||
processingConfig ||
|
||||
processingValidate ||
|
||||
(isEnabled && (!privateKey || !certificateChain)) ||
|
||||
(privateKey && !valid_key) ||
|
||||
(certificateChain && !valid_cert) ||
|
||||
(privateKey && certificateChain && !valid_pair);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
@ -180,13 +181,20 @@ let Form = (props) => {
|
|||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<label className="form__label form__label--bold" htmlFor="certificate_chain">
|
||||
<label
|
||||
className="form__label form__label--bold"
|
||||
htmlFor="certificate_chain"
|
||||
>
|
||||
<Trans>encryption_certificates</Trans>
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans
|
||||
values={{ link: 'letsencrypt.org' }}
|
||||
components={[<a href="https://letsencrypt.org/" key="0">link</a>]}
|
||||
components={[
|
||||
<a href="https://letsencrypt.org/" key="0">
|
||||
link
|
||||
</a>,
|
||||
]}
|
||||
>
|
||||
encryption_certificates_desc
|
||||
</Trans>
|
||||
|
@ -202,49 +210,52 @@ let Form = (props) => {
|
|||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{certificateChain &&
|
||||
{certificateChain && (
|
||||
<Fragment>
|
||||
<div className="form__label form__label--bold">
|
||||
<Trans>encryption_status</Trans>:
|
||||
</div>
|
||||
<ul className="encryption__list">
|
||||
<li className={valid_chain ? 'text-success' : 'text-danger'}>
|
||||
{valid_chain ?
|
||||
<li
|
||||
className={valid_chain ? 'text-success' : 'text-danger'}
|
||||
>
|
||||
{valid_chain ? (
|
||||
<Trans>encryption_chain_valid</Trans>
|
||||
: <Trans>encryption_chain_invalid</Trans>
|
||||
}
|
||||
) : (
|
||||
<Trans>encryption_chain_invalid</Trans>
|
||||
)}
|
||||
</li>
|
||||
{valid_cert &&
|
||||
{valid_cert && (
|
||||
<Fragment>
|
||||
{subject &&
|
||||
{subject && (
|
||||
<li>
|
||||
<Trans>encryption_subject</Trans>:
|
||||
{subject}
|
||||
</li>
|
||||
}
|
||||
{issuer &&
|
||||
)}
|
||||
{issuer && (
|
||||
<li>
|
||||
<Trans>encryption_issuer</Trans>:
|
||||
{issuer}
|
||||
</li>
|
||||
}
|
||||
{not_after && not_after !== EMPTY_DATE &&
|
||||
)}
|
||||
{not_after && not_after !== EMPTY_DATE && (
|
||||
<li>
|
||||
<Trans>encryption_expire</Trans>:
|
||||
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
|
||||
</li>
|
||||
}
|
||||
{dns_names &&
|
||||
)}
|
||||
{dns_names && (
|
||||
<li>
|
||||
<Trans>encryption_hostnames</Trans>:
|
||||
{dns_names}
|
||||
</li>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</ul>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -266,35 +277,34 @@ let Form = (props) => {
|
|||
disabled={!isEnabled}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{privateKey &&
|
||||
{privateKey && (
|
||||
<Fragment>
|
||||
<div className="form__label form__label--bold">
|
||||
<Trans>encryption_status</Trans>:
|
||||
</div>
|
||||
<ul className="encryption__list">
|
||||
<li className={valid_key ? 'text-success' : 'text-danger'}>
|
||||
{valid_key ?
|
||||
{valid_key ? (
|
||||
<Trans values={{ type: key_type }}>
|
||||
encryption_key_valid
|
||||
</Trans>
|
||||
: <Trans values={{ type: key_type }}>
|
||||
) : (
|
||||
<Trans values={{ type: key_type }}>
|
||||
encryption_key_invalid
|
||||
</Trans>
|
||||
}
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{warning_validation &&
|
||||
{warning_validation && (
|
||||
<div className="col-12">
|
||||
<p className="text-danger">
|
||||
{warning_validation}
|
||||
</p>
|
||||
<p className="text-danger">{warning_validation}</p>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="btn-list mt-2">
|
||||
|
|
|
@ -6,11 +6,17 @@ import debounce from 'lodash/debounce';
|
|||
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
|
||||
import Form from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
import Loading from '../../ui/Loading';
|
||||
|
||||
class Encryption extends Component {
|
||||
componentDidMount() {
|
||||
if (this.props.encryption.enabled) {
|
||||
this.props.validateTlsConfig(this.props.encryption);
|
||||
const { getTlsStatus, validateTlsConfig, encryption } = this.props;
|
||||
|
||||
getTlsStatus();
|
||||
|
||||
if (encryption.enabled) {
|
||||
validateTlsConfig(encryption);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +42,9 @@ class Encryption extends Component {
|
|||
|
||||
return (
|
||||
<div className="encryption">
|
||||
{encryption &&
|
||||
<PageTitle title={t('encryption_settings')} />
|
||||
{encryption.processing && <Loading />}
|
||||
{!encryption.processing && (
|
||||
<Card
|
||||
title={t('encryption_title')}
|
||||
subtitle={t('encryption_desc')}
|
||||
|
@ -58,7 +66,7 @@ class Encryption extends Component {
|
|||
{...this.props.encryption}
|
||||
/>
|
||||
</Card>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,12 +2,6 @@ import React, { Component, Fragment } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { withNamespaces, Trans } from 'react-i18next';
|
||||
|
||||
import Upstream from './Upstream';
|
||||
import Dhcp from './Dhcp';
|
||||
import Encryption from './Encryption';
|
||||
import Clients from './Clients';
|
||||
import AutoClients from './Clients/AutoClients';
|
||||
import Access from './Access';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import Loading from '../ui/Loading';
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
|
@ -41,10 +35,6 @@ class Settings extends Component {
|
|||
|
||||
componentDidMount() {
|
||||
this.props.initSettings(this.settings);
|
||||
this.props.getDhcpStatus();
|
||||
this.props.getDhcpInterfaces();
|
||||
this.props.getTlsStatus();
|
||||
this.props.getAccessList();
|
||||
}
|
||||
|
||||
renderSettings = (settings) => {
|
||||
|
@ -69,72 +59,20 @@ class Settings extends Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
settings, dashboard, clients, access, t,
|
||||
} = this.props;
|
||||
const { settings, t } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<PageTitle title={t('settings')} />
|
||||
<PageTitle title={t('general_settings')} />
|
||||
{settings.processing && <Loading />}
|
||||
{!settings.processing && (
|
||||
<div className="content">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<Card
|
||||
title={t('general_settings')}
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Card bodyType="card-body box-body--settings">
|
||||
<div className="form">
|
||||
{this.renderSettings(settings.settingsList)}
|
||||
</div>
|
||||
</Card>
|
||||
<Upstream
|
||||
upstreamDns={dashboard.upstreamDns}
|
||||
bootstrapDns={dashboard.bootstrapDns}
|
||||
allServers={dashboard.allServers}
|
||||
setUpstream={this.props.setUpstream}
|
||||
testUpstream={this.props.testUpstream}
|
||||
processingTestUpstream={settings.processingTestUpstream}
|
||||
processingSetUpstream={settings.processingSetUpstream}
|
||||
/>
|
||||
{!dashboard.processingTopStats && !dashboard.processingClients && (
|
||||
<Fragment>
|
||||
<Clients
|
||||
clients={dashboard.clients}
|
||||
topStats={dashboard.topStats}
|
||||
isModalOpen={clients.isModalOpen}
|
||||
modalClientName={clients.modalClientName}
|
||||
modalType={clients.modalType}
|
||||
addClient={this.props.addClient}
|
||||
updateClient={this.props.updateClient}
|
||||
deleteClient={this.props.deleteClient}
|
||||
toggleClientModal={this.props.toggleClientModal}
|
||||
processingAdding={clients.processingAdding}
|
||||
processingDeleting={clients.processingDeleting}
|
||||
processingUpdating={clients.processingUpdating}
|
||||
/>
|
||||
<AutoClients
|
||||
autoClients={dashboard.autoClients}
|
||||
topStats={dashboard.topStats}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
<Access access={access} setAccessList={this.props.setAccessList} />
|
||||
<Encryption
|
||||
encryption={this.props.encryption}
|
||||
setTlsConfig={this.props.setTlsConfig}
|
||||
validateTlsConfig={this.props.validateTlsConfig}
|
||||
/>
|
||||
<Dhcp
|
||||
dhcp={this.props.dhcp}
|
||||
toggleDhcp={this.props.toggleDhcp}
|
||||
getDhcpStatus={this.props.getDhcpStatus}
|
||||
findActiveDhcp={this.props.findActiveDhcp}
|
||||
setDhcpConfig={this.props.setDhcpConfig}
|
||||
addStaticLease={this.props.addStaticLease}
|
||||
removeStaticLease={this.props.removeStaticLease}
|
||||
toggleLeaseModal={this.props.toggleLeaseModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -149,8 +87,6 @@ Settings.propTypes = {
|
|||
settings: PropTypes.object,
|
||||
settingsList: PropTypes.object,
|
||||
toggleSetting: PropTypes.func,
|
||||
handleUpstreamChange: PropTypes.func,
|
||||
setUpstream: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.dropdown-item.active,
|
||||
.dropdown-item:active {
|
||||
background-color: #66b574;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
cursor: default;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withNamespaces } from 'react-i18next';
|
||||
import enhanceWithClickOutside from 'react-click-outside';
|
||||
|
||||
import './Dropdown.css';
|
||||
|
||||
class Dropdown extends Component {
|
||||
state = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
toggleDropdown = () => {
|
||||
this.setState(prevState => ({ isOpen: !prevState.isOpen }));
|
||||
};
|
||||
|
||||
hideDropdown = () => {
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
handleClickOutside = () => {
|
||||
if (this.state.isOpen) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
controlClassName,
|
||||
menuClassName,
|
||||
baseClassName,
|
||||
icon,
|
||||
children,
|
||||
} = this.props;
|
||||
|
||||
const { isOpen } = this.state;
|
||||
|
||||
const dropdownClass = classnames({
|
||||
[baseClassName]: true,
|
||||
show: isOpen,
|
||||
});
|
||||
|
||||
const dropdownMenuClass = classnames({
|
||||
[menuClassName]: true,
|
||||
show: isOpen,
|
||||
});
|
||||
|
||||
const ariaSettings = isOpen ? 'true' : 'false';
|
||||
|
||||
return (
|
||||
<div className={dropdownClass}>
|
||||
<a
|
||||
className={controlClassName}
|
||||
aria-expanded={ariaSettings}
|
||||
onClick={this.toggleDropdown}
|
||||
>
|
||||
{icon && (
|
||||
<svg className="nav-icon">
|
||||
<use xlinkHref={`#${icon}`} />
|
||||
</svg>
|
||||
)}
|
||||
{label}
|
||||
</a>
|
||||
<div className={dropdownMenuClass} onClick={this.hideDropdown}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Dropdown.defaultProps = {
|
||||
baseClassName: 'dropdown',
|
||||
menuClassName: 'dropdown-menu dropdown-menu-arrow',
|
||||
controlClassName: '',
|
||||
};
|
||||
|
||||
Dropdown.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
controlClassName: PropTypes.node.isRequired,
|
||||
menuClassName: PropTypes.string.isRequired,
|
||||
baseClassName: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
export default withNamespaces()(enhanceWithClickOutside(Dropdown));
|
|
@ -31,6 +31,30 @@ const Icons = () => (
|
|||
<symbol id="delete" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m3 6h2 16"/><path d="m19 6v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2-2v-14m3 0v-2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="m10 11v6"/><path d="m14 11v6"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="back" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m19 12h-14"/><path d="m12 19-7-7 7-7"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="dashboard" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2-2z"/><path d="m9 22v-10h6v10"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="filters" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m22 3h-20l8 9.46v6.54l4 2v-8.54z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="log" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="m14 2h-8a2 2 0 0 0 -2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-12z"/><path d="m14 2v6h6"/><path d="m16 13h-8"/><path d="m16 17h-8"/><path d="m10 9h-1-1"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="setup" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line>
|
||||
</symbol>
|
||||
|
||||
<symbol id="settings" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="3"/><path d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { addErrorToast } from '../actions';
|
||||
import { addClient, updateClient, deleteClient, toggleClientModal } from '../actions/clients';
|
||||
import Clients from '../components/Settings/Clients';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dashboard, clients } = state;
|
||||
const props = {
|
||||
dashboard,
|
||||
clients,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addErrorToast,
|
||||
addClient,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
toggleClientModal,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Clients);
|
|
@ -0,0 +1,38 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
addErrorToast,
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
toggleLeaseModal,
|
||||
addStaticLease,
|
||||
removeStaticLease,
|
||||
} from '../actions';
|
||||
import Dhcp from '../components/Settings/Dhcp';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dhcp } = state;
|
||||
const props = {
|
||||
dhcp,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addErrorToast,
|
||||
toggleDhcp,
|
||||
getDhcpStatus,
|
||||
getDhcpInterfaces,
|
||||
setDhcpConfig,
|
||||
findActiveDhcp,
|
||||
toggleLeaseModal,
|
||||
addStaticLease,
|
||||
removeStaticLease,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Dhcp);
|
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions';
|
||||
import Dns from '../components/Settings/Dns';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { dashboard, settings } = state;
|
||||
const props = {
|
||||
dashboard,
|
||||
settings,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
handleUpstreamChange,
|
||||
setUpstream,
|
||||
testUpstream,
|
||||
addErrorToast,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Dns);
|
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { addErrorToast } from '../actions';
|
||||
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
|
||||
import Encryption from '../components/Settings/Encryption';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { encryption } = state;
|
||||
const props = {
|
||||
encryption,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addErrorToast,
|
||||
getTlsStatus,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Encryption);
|
|
@ -177,3 +177,5 @@ export const CLIENT_ID = {
|
|||
MAC: 'mac',
|
||||
IP: 'ip',
|
||||
};
|
||||
|
||||
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients'];
|
||||
|
|
Loading…
Reference in New Issue