+ client: handle per-client settings

This commit is contained in:
Ildar Kamalov
2019-05-22 17:59:57 +03:00
committed by Simon Zolin
parent 22c7efd2d1
commit 22d3c38df2
16 changed files with 863 additions and 40 deletions

View File

@@ -5,6 +5,10 @@
min-height: 26px;
}
.logs__row--center {
justify-content: center;
}
.logs__row--overflow {
overflow: hidden;
}

View File

@@ -0,0 +1,215 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import { renderField, renderSelectField, ipv4, mac, required } from '../../../helpers/form';
import { CLIENT_ID } from '../../../helpers/constants';
let Form = (props) => {
const {
t,
handleSubmit,
reset,
pristine,
submitting,
clientIdentifier,
useGlobalSettings,
toggleClientModal,
processingAdding,
processingUpdating,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form__group">
<div className="form-inline mb-3">
<strong className="mr-3">
<Trans>client_identifier</Trans>
</strong>
<label className="mr-3">
<Field
name="identifier"
component={renderField}
type="radio"
className="form-control mr-2"
value="ip"
/>{' '}
<Trans>ip_address</Trans>
</label>
<label>
<Field
name="identifier"
component={renderField}
type="radio"
className="form-control mr-2"
value="mac"
/>{' '}
MAC
</label>
</div>
{clientIdentifier === CLIENT_ID.IP && (
<div className="form__group">
<Field
id="ip"
name="ip"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv4, required]}
/>
</div>
)}
{clientIdentifier === CLIENT_ID.MAC && (
<div className="form__group">
<Field
id="mac"
name="mac"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_enter_mac')}
validate={[mac, required]}
/>
</div>
)}
<div className="form__desc">
<Trans
components={[
<a href="#settings_dhcp" key="0">
link
</a>,
]}
>
client_identifier_desc
</Trans>
</div>
</div>
<div className="form__group">
<Field
id="name"
name="name"
component={renderField}
type="text"
className="form-control"
placeholder={t('form_client_name')}
validate={[required]}
/>
</div>
<div className="mb-4">
<strong>Settings</strong>
</div>
<div className="form__group">
<Field
name="use_global_settings"
type="checkbox"
component={renderSelectField}
placeholder={t('client_global_settings')}
/>
</div>
<div className="form__group">
<Field
name="filtering_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('block_domain_use_filters_and_hosts')}
disabled={useGlobalSettings}
/>
</div>
<div className="form__group">
<Field
name="safebrowsing_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('use_adguard_browsing_sec')}
disabled={useGlobalSettings}
/>
</div>
<div className="form__group">
<Field
name="parental_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('use_adguard_parental')}
disabled={useGlobalSettings}
/>
</div>
<div className="form__group">
<Field
name="safesearch_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('enforce_safe_search')}
disabled={useGlobalSettings}
/>
</div>
</div>
<div className="modal-footer">
<div className="btn-list">
<button
type="button"
className="btn btn-secondary btn-standard"
disabled={submitting}
onClick={() => {
reset();
toggleClientModal();
}}
>
<Trans>cancel_btn</Trans>
</button>
<button
type="submit"
className="btn btn-success btn-standard"
disabled={submitting || pristine || processingAdding || processingUpdating}
>
<Trans>save_btn</Trans>
</button>
</div>
</div>
</form>
);
};
Form.propTypes = {
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
toggleClientModal: PropTypes.func.isRequired,
clientIdentifier: PropTypes.string,
useGlobalSettings: PropTypes.bool,
t: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
};
const selector = formValueSelector('clientForm');
Form = connect((state) => {
const clientIdentifier = selector(state, 'identifier');
const useGlobalSettings = selector(state, 'use_global_settings');
return {
clientIdentifier,
useGlobalSettings,
};
})(Form);
export default flow([
withNamespaces(),
reduxForm({
form: 'clientForm',
enableReinitialize: true,
}),
])(Form);

View File

@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, withNamespaces } from 'react-i18next';
import ReactModal from 'react-modal';
import Form from './Form';
const Modal = (props) => {
const {
isModalOpen,
currentClientData,
handleSubmit,
toggleClientModal,
processingAdding,
processingUpdating,
} = props;
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
closeTimeoutMS={0}
isOpen={isModalOpen}
onRequestClose={() => toggleClientModal()}
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
<Trans>client_new</Trans>
</h4>
<button type="button" className="close" onClick={() => toggleClientModal()}>
<span className="sr-only">Close</span>
</button>
</div>
<Form
initialValues={{
...currentClientData,
}}
onSubmit={handleSubmit}
toggleClientModal={toggleClientModal}
processingAdding={processingAdding}
processingUpdating={processingUpdating}
/>
</div>
</ReactModal>
);
};
Modal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
currentClientData: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
toggleClientModal: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired,
};
export default withNamespaces()(Modal);

View File

@@ -0,0 +1,252 @@
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 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: 'ip',
use_global_settings: true,
};
};
getStats = (ip, stats) => {
if (stats && stats.top_clients) {
return stats.top_clients[ip];
}
return '';
};
columns = [
{
Header: this.props.t('table_client'),
accessor: 'ip',
Cell: (row) => {
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>
);
} else 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>
);
}
return '';
},
},
{
Header: this.props.t('table_name'),
accessor: 'name',
Cell: this.cellWrap,
},
{
Header: this.props.t('settings'),
accessor: 'use_global_settings',
maxWidth: 180,
minWidth: 150,
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: 220,
minWidth: 150,
Cell: (row) => {
const clientName = row.original.name;
const {
toggleClientModal,
deleteClient,
processingDeleting,
processingUpdating,
} = this.props;
return (
<div className="logs__row logs__row--center">
<button
type="button"
className="btn btn-outline-primary btn-sm mr-2"
onClick={() =>
toggleClientModal({
type: MODAL_TYPE.EDIT,
name: clientName,
})
}
disabled={processingUpdating}
>
<Trans>edit_table_action</Trans>
</button>
<button
type="button"
className="btn btn-outline-secondary btn-sm"
onClick={() => deleteClient({ name: clientName })}
disabled={processingDeleting}
>
<Trans>delete_table_action</Trans>
</button>
</div>
);
},
},
];
render() {
const {
t,
clients,
isModalOpen,
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}
noDataText={t('dhcp_leases_not_found')}
className="-striped -highlight card-table-overflow"
showPagination={true}
defaultPageSize={10}
minRows={5}
resizable={false}
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')}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleClientModal(MODAL_TYPE.ADD)}
disabled={processingAdding}
>
<Trans>add_client</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
toggleClientModal={toggleClientModal}
currentClientData={currentClientData}
handleSubmit={this.handleSubmit}
processingAdding={processingAdding}
processingUpdating={processingUpdating}
/>
</Fragment>
</Card>
);
}
}
Clients.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()(Clients);

View File

@@ -4,6 +4,7 @@ import { withNamespaces, Trans } from 'react-i18next';
import Upstream from './Upstream';
import Dhcp from './Dhcp';
import Encryption from './Encryption';
import Clients from './Clients';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
@@ -46,29 +47,38 @@ class Settings extends Component {
return Object.keys(settings).map((key) => {
const setting = settings[key];
const { enabled } = setting;
return (<Checkbox
key={key}
{...settings[key]}
handleChange={() => this.props.toggleSetting(key, enabled)}
/>);
return (
<Checkbox
key={key}
{...settings[key]}
handleChange={() => this.props.toggleSetting(key, enabled)}
/>
);
});
}
return (
<div><Trans>no_settings</Trans></div>
<div>
<Trans>no_settings</Trans>
</div>
);
}
};
render() {
const { settings, dashboard, t } = this.props;
const {
settings, dashboard, clients, t,
} = this.props;
return (
<Fragment>
<PageTitle title={ t('settings') } />
<PageTitle title={t('settings')} />
{settings.processing && <Loading />}
{!settings.processing &&
{!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
title={t('general_settings')}
bodyType="card-body box-body--settings"
>
<div className="form">
{this.renderSettings(settings.settingsList)}
</div>
@@ -82,6 +92,22 @@ class Settings extends Component {
processingTestUpstream={settings.processingTestUpstream}
processingSetUpstream={settings.processingSetUpstream}
/>
{!dashboard.processingTopStats && !dashboard.processingClients && (
<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}
/>
)}
<Encryption
encryption={this.props.encryption}
setTlsConfig={this.props.setTlsConfig}
@@ -97,7 +123,7 @@ class Settings extends Component {
</div>
</div>
</div>
}
)}
</Fragment>
);
}

View File

@@ -5,7 +5,7 @@
overflow-x: hidden;
overflow-y: auto;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
z-index: 105;
}
.ReactModal__Overlay--after-open {
@@ -38,3 +38,9 @@
border: none;
background-color: transparent;
}
@media (min-width: 576px) {
.modal-dialog--clients {
max-width: 650px;
}
}