+ client: split settings page into several pages

This commit is contained in:
Ildar Kamalov 2019-06-03 15:41:45 +03:00
parent 1d09ff0562
commit cf53653cfa
22 changed files with 882 additions and 492 deletions

View File

@ -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",

View File

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

View File

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

View File

@ -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>
</NavLink>
</li>
<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>
<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));

View File

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

View File

@ -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')}
/>
<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>
<Fragment>
<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}
/>
<AutoClients
autoClients={dashboard.autoClients}
topStats={dashboard.topStats}
/>
</Fragment>
)}
</Fragment>
);
}
}
Clients.propTypes = {
t: PropTypes.func.isRequired,
dashboard: PropTypes.object.isRequired,
clients: PropTypes.array.isRequired,
topStats: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired,

View File

@ -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) {
@ -121,21 +124,19 @@ class Dhcp extends Component {
</Accordion>
</div>
</div>
<hr className="mt-4 mb-4"/>
<hr className="mt-4 mb-4" />
</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,
@ -144,13 +145,13 @@ class Dhcp extends Component {
dhcp_dynamic_ip_found
</Trans>
</div>
<hr className="mt-4 mb-4"/>
<hr className="mt-4 mb-4" />
</Fragment>
);
}
return '';
}
};
render() {
const { t, dhcp } = this.props;
@ -158,93 +159,101 @@ 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">
<div className="dhcp">
{!dhcp.processing &&
<Fragment>
<Form
onSubmit={this.handleFormSubmit}
initialValues={{
interface_name,
...values,
}}
interfaces={dhcp.interfaces}
processingConfig={dhcp.processingConfig}
processingInterfaces={dhcp.processingInterfaces}
enabled={enabled}
/>
<hr/>
<div className="card-actions mb-3">
{this.getToggleDhcpButton()}
<button
type="button"
className={statusButtonClass}
onClick={() =>
this.props.findActiveDhcp(interface_name)
}
disabled={
enabled
|| !interface_name
|| dhcp.processingConfig
}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
{!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 &&
<PageTitle title={t('dhcp_settings')} />
{(dhcp.processing || dhcp.processingInterfaces) && <Loading />}
{!dhcp.processing && !dhcp.processingInterfaces && (
<Fragment>
<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">
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={dhcp.staticLeases}
isModalOpen={dhcp.isModalOpen}
addStaticLease={this.props.addStaticLease}
removeStaticLease={this.props.removeStaticLease}
toggleLeaseModal={this.props.toggleLeaseModal}
processingAdding={dhcp.processingAdding}
processingDeleting={dhcp.processingDeleting}
<Card
title={t('dhcp_title')}
subtitle={t('dhcp_description')}
bodyType="card-body box-body--settings"
>
<div className="dhcp">
<Fragment>
<Form
onSubmit={this.handleFormSubmit}
initialValues={{
interface_name,
...values,
}}
interfaces={dhcp.interfaces}
processingConfig={dhcp.processingConfig}
processingInterfaces={dhcp.processingInterfaces}
enabled={enabled}
/>
</div>
<div className="col-12">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => this.props.toggleLeaseModal()}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
</div>
<hr />
<div className="card-actions mb-3">
{this.getToggleDhcpButton()}
<button
type="button"
className={statusButtonClass}
onClick={() =>
this.props.findActiveDhcp(interface_name)
}
disabled={
enabled || !interface_name || dhcp.processingConfig
}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
{!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.config.enabled && (
<Fragment>
<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"
>
<div className="row">
<div className="col-12">
<StaticLeases
staticLeases={dhcp.staticLeases}
isModalOpen={dhcp.isModalOpen}
addStaticLease={this.props.addStaticLease}
removeStaticLease={this.props.removeStaticLease}
toggleLeaseModal={this.props.toggleLeaseModal}
processingAdding={dhcp.processingAdding}
processingDeleting={dhcp.processingDeleting}
/>
</div>
<div className="col-12">
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => this.props.toggleLeaseModal()}
>
<Trans>dhcp_add_static_lease</Trans>
</button>
</div>
</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,
};

View File

@ -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>,
]}
@ -18,14 +23,19 @@ const Examples = props => (
<Trans>examples_title</Trans>:
<ol className="leading-loose">
<li>
<code>1.1.1.1</code> - { props.t('example_upstream_regular') }
<code>1.1.1.1</code> - {props.t('example_upstream_regular')}
</li>
<li>
<code>tls://1dot1dot1dot1.cloudflare-dns.com</code> &nbsp;
<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>,
]}

View File

@ -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) => {
@ -58,7 +58,7 @@ let Form = (props) => {
</div>
<div className="col-12">
<Examples />
<hr/>
<hr />
</div>
<div className="col-12">
<div className="form__group">
@ -84,11 +84,13 @@ let Form = (props) => {
<button
type="button"
className={testButtonClass}
onClick={() => testUpstream({
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
})}
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);

View File

@ -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 {
@ -26,8 +26,8 @@ class Upstream extends Component {
return (
<Card
title={ t('upstream_dns') }
subtitle={ t('upstream_dns_hint') }
title={t('upstream_dns')}
subtitle={t('upstream_dns_hint')}
bodyType="card-body box-body--settings"
>
<div className="row">

View File

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

View File

@ -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}>
@ -91,7 +92,7 @@ let Form = (props) => {
<div className="form__desc">
<Trans>encryption_enable_desc</Trans>
</div>
<hr/>
<hr />
</div>
<div className="col-12">
<label className="form__label" htmlFor="server_name">
@ -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>:&nbsp;
{subject}
</li>
}
{issuer &&
)}
{issuer && (
<li>
<Trans>encryption_issuer</Trans>:&nbsp;
{issuer}
</li>
}
{not_after && not_after !== EMPTY_DATE &&
)}
{not_after && not_after !== EMPTY_DATE && (
<li>
<Trans>encryption_expire</Trans>:&nbsp;
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
</li>
}
{dns_names &&
)}
{dns_names && (
<li>
<Trans>encryption_hostnames</Trans>:&nbsp;
{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">

View File

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

View File

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

View File

@ -0,0 +1,8 @@
.dropdown-item.active,
.dropdown-item:active {
background-color: #66b574;
}
.dropdown-menu {
cursor: default;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -177,3 +177,5 @@ export const CLIENT_ID = {
MAC: 'mac',
IP: 'ip',
};
export const SETTINGS_URLS = ['/encryption', '/dhcp', '/dns', '/settings', '/clients'];