+ client: handle per-client settings
This commit is contained in:
parent
22c7efd2d1
commit
22d3c38df2
|
@ -19,6 +19,7 @@
|
||||||
"dhcp_config_saved": "Saved DHCP server config",
|
"dhcp_config_saved": "Saved DHCP server config",
|
||||||
"form_error_required": "Required field",
|
"form_error_required": "Required field",
|
||||||
"form_error_ip_format": "Invalid IPv4 format",
|
"form_error_ip_format": "Invalid IPv4 format",
|
||||||
|
"form_error_mac_format": "Invalid MAC format",
|
||||||
"form_error_positive": "Must be greater than 0",
|
"form_error_positive": "Must be greater than 0",
|
||||||
"dhcp_form_gateway_input": "Gateway IP",
|
"dhcp_form_gateway_input": "Gateway IP",
|
||||||
"dhcp_form_subnet_input": "Subnet mask",
|
"dhcp_form_subnet_input": "Subnet mask",
|
||||||
|
@ -105,6 +106,7 @@
|
||||||
"rules_count_table_header": "Rules count",
|
"rules_count_table_header": "Rules count",
|
||||||
"last_time_updated_table_header": "Last time updated",
|
"last_time_updated_table_header": "Last time updated",
|
||||||
"actions_table_header": "Actions",
|
"actions_table_header": "Actions",
|
||||||
|
"edit_table_action": "Edit",
|
||||||
"delete_table_action": "Delete",
|
"delete_table_action": "Delete",
|
||||||
"filters_and_hosts": "Filters and hosts blocklists",
|
"filters_and_hosts": "Filters and hosts blocklists",
|
||||||
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
|
"filters_and_hosts_hint": "AdGuard Home understands basic adblock rules and hosts files syntax.",
|
||||||
|
@ -263,5 +265,25 @@
|
||||||
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
"dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
|
||||||
"update_now": "Update now",
|
"update_now": "Update now",
|
||||||
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
"update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps<\/a> to update manually.",
|
||||||
"processing_update": "Please wait, AdGuard Home is being updated"
|
"processing_update": "Please wait, AdGuard Home is being updated",
|
||||||
|
"clients_title": "Clients",
|
||||||
|
"clients_desc": "Configure devices connected to AdGuard Home",
|
||||||
|
"settings_global": "Global",
|
||||||
|
"settings_custom": "Custom",
|
||||||
|
"add_client": "Add Client",
|
||||||
|
"table_client": "Client",
|
||||||
|
"table_name": "Name",
|
||||||
|
"save_btn": "Save",
|
||||||
|
"client_new": "New Client",
|
||||||
|
"client_identifier": "Identifier",
|
||||||
|
"ip_address": "IP address",
|
||||||
|
"client_identifier_desc": "Clients can be identified by the IP address or MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
|
||||||
|
"form_enter_ip": "Enter IP",
|
||||||
|
"form_enter_mac": "Enter MAC",
|
||||||
|
"form_client_name": "Enter client name",
|
||||||
|
"client_global_settings": "Use global settings",
|
||||||
|
"client_deleted": "Client \"{{key}}\" successfully deleted",
|
||||||
|
"client_added": "Client \"{{key}}\" successfully added",
|
||||||
|
"client_updated": "Client \"{{key}}\" successfully updated",
|
||||||
|
"table_statistics": "Statistics (last 24 hours)"
|
||||||
}
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import Api from '../api/Api';
|
||||||
|
import { addErrorToast, addSuccessToast, getClients } from './index';
|
||||||
|
import { CLIENT_ID } from '../helpers/constants';
|
||||||
|
|
||||||
|
const apiClient = new Api();
|
||||||
|
|
||||||
|
export const toggleClientModal = createAction('TOGGLE_CLIENT_MODAL');
|
||||||
|
|
||||||
|
export const addClientRequest = createAction('ADD_CLIENT_REQUEST');
|
||||||
|
export const addClientFailure = createAction('ADD_CLIENT_FAILURE');
|
||||||
|
export const addClientSuccess = createAction('ADD_CLIENT_SUCCESS');
|
||||||
|
|
||||||
|
export const addClient = config => async (dispatch) => {
|
||||||
|
dispatch(addClientRequest());
|
||||||
|
try {
|
||||||
|
let data;
|
||||||
|
if (config.identifier === CLIENT_ID.MAC) {
|
||||||
|
const { ip, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { ...values };
|
||||||
|
} else {
|
||||||
|
const { mac, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { ...values };
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.addClient(data);
|
||||||
|
dispatch(addClientSuccess());
|
||||||
|
dispatch(toggleClientModal());
|
||||||
|
dispatch(addSuccessToast(t('client_added', { key: config.name })));
|
||||||
|
dispatch(getClients());
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(toggleClientModal());
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(addClientFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteClientRequest = createAction('DELETE_CLIENT_REQUEST');
|
||||||
|
export const deleteClientFailure = createAction('DELETE_CLIENT_FAILURE');
|
||||||
|
export const deleteClientSuccess = createAction('DELETE_CLIENT_SUCCESS');
|
||||||
|
|
||||||
|
export const deleteClient = config => async (dispatch) => {
|
||||||
|
dispatch(deleteClientRequest());
|
||||||
|
try {
|
||||||
|
await apiClient.deleteClient(config);
|
||||||
|
dispatch(deleteClientSuccess());
|
||||||
|
dispatch(addSuccessToast(t('client_deleted', { key: config.name })));
|
||||||
|
dispatch(getClients());
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(deleteClientFailure());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateClientRequest = createAction('UPDATE_CLIENT_REQUEST');
|
||||||
|
export const updateClientFailure = createAction('UPDATE_CLIENT_FAILURE');
|
||||||
|
export const updateClientSuccess = createAction('UPDATE_CLIENT_SUCCESS');
|
||||||
|
|
||||||
|
export const updateClient = (config, name) => async (dispatch) => {
|
||||||
|
dispatch(updateClientRequest());
|
||||||
|
try {
|
||||||
|
let data;
|
||||||
|
if (config.identifier === CLIENT_ID.MAC) {
|
||||||
|
const { ip, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { name, data: { ...values } };
|
||||||
|
} else {
|
||||||
|
const { mac, identifier, ...values } = config;
|
||||||
|
|
||||||
|
data = { name, data: { ...values } };
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.updateClient(data);
|
||||||
|
dispatch(updateClientSuccess());
|
||||||
|
dispatch(toggleClientModal());
|
||||||
|
dispatch(addSuccessToast(t('client_updated', { key: name })));
|
||||||
|
dispatch(getClients());
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(toggleClientModal());
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(updateClientFailure());
|
||||||
|
}
|
||||||
|
};
|
|
@ -4,7 +4,7 @@ import { t } from 'i18next';
|
||||||
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
import { showLoading, hideLoading } from 'react-redux-loading-bar';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea } from '../helpers/helpers';
|
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs, normalizeTextarea, sortClients } from '../helpers/helpers';
|
||||||
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
|
import { SETTINGS_NAMES, CHECK_TIMEOUT } from '../helpers/constants';
|
||||||
import Api from '../api/Api';
|
import Api from '../api/Api';
|
||||||
|
|
||||||
|
@ -213,14 +213,36 @@ export const getClientsSuccess = createAction('GET_CLIENTS_SUCCESS');
|
||||||
export const getClients = () => async (dispatch) => {
|
export const getClients = () => async (dispatch) => {
|
||||||
dispatch(getClientsRequest());
|
dispatch(getClientsRequest());
|
||||||
try {
|
try {
|
||||||
const clients = await apiClient.getGlobalClients();
|
const clients = await apiClient.getClients();
|
||||||
dispatch(getClientsSuccess(clients));
|
const sortedClients = sortClients(clients);
|
||||||
|
dispatch(getClientsSuccess(sortedClients));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(getClientsFailure());
|
dispatch(getClientsFailure());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
||||||
|
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
||||||
|
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
||||||
|
|
||||||
|
export const getTopStats = () => async (dispatch, getState) => {
|
||||||
|
dispatch(getTopStatsRequest());
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
const state = getState();
|
||||||
|
if (state.dashboard.isCoreRunning) {
|
||||||
|
clearInterval(timer);
|
||||||
|
try {
|
||||||
|
const stats = await apiClient.getGlobalStatsTop();
|
||||||
|
dispatch(getTopStatsSuccess(stats));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(getTopStatsFailure(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
||||||
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||||
|
@ -232,6 +254,7 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||||
dispatch(dnsStatusSuccess(dnsStatus));
|
dispatch(dnsStatusSuccess(dnsStatus));
|
||||||
dispatch(getVersion());
|
dispatch(getVersion());
|
||||||
dispatch(getClients());
|
dispatch(getClients());
|
||||||
|
dispatch(getTopStats());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(initSettingsFailure());
|
dispatch(initSettingsFailure());
|
||||||
|
@ -289,27 +312,6 @@ export const getStats = () => async (dispatch) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
|
|
||||||
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
|
|
||||||
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
|
|
||||||
|
|
||||||
export const getTopStats = () => async (dispatch, getState) => {
|
|
||||||
dispatch(getTopStatsRequest());
|
|
||||||
const timer = setInterval(async () => {
|
|
||||||
const state = getState();
|
|
||||||
if (state.dashboard.isCoreRunning) {
|
|
||||||
clearInterval(timer);
|
|
||||||
try {
|
|
||||||
const stats = await apiClient.getGlobalStatsTop();
|
|
||||||
dispatch(getTopStatsSuccess(stats));
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(addErrorToast({ error }));
|
|
||||||
dispatch(getTopStatsFailure(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||||
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||||
|
|
|
@ -409,4 +409,42 @@ export default class Api {
|
||||||
};
|
};
|
||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-client settings
|
||||||
|
GET_CLIENTS = { path: 'clients', method: 'GET' }
|
||||||
|
ADD_CLIENT = { path: 'clients/add', method: 'POST' }
|
||||||
|
DELETE_CLIENT = { path: 'clients/delete', method: 'POST' }
|
||||||
|
UPDATE_CLIENT = { path: 'clients/update', method: 'POST' }
|
||||||
|
|
||||||
|
getClients() {
|
||||||
|
const { path, method } = this.GET_CLIENTS;
|
||||||
|
return this.makeRequest(path, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
addClient(config) {
|
||||||
|
const { path, method } = this.ADD_CLIENT;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteClient(config) {
|
||||||
|
const { path, method } = this.DELETE_CLIENT;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClient(config) {
|
||||||
|
const { path, method } = this.UPDATE_CLIENT;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
min-height: 26px;
|
min-height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs__row--center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.logs__row--overflow {
|
.logs__row--overflow {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -4,6 +4,7 @@ import { withNamespaces, Trans } from 'react-i18next';
|
||||||
import Upstream from './Upstream';
|
import Upstream from './Upstream';
|
||||||
import Dhcp from './Dhcp';
|
import Dhcp from './Dhcp';
|
||||||
import Encryption from './Encryption';
|
import Encryption from './Encryption';
|
||||||
|
import Clients from './Clients';
|
||||||
import Checkbox from '../ui/Checkbox';
|
import Checkbox from '../ui/Checkbox';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
|
@ -46,29 +47,38 @@ class Settings extends Component {
|
||||||
return Object.keys(settings).map((key) => {
|
return Object.keys(settings).map((key) => {
|
||||||
const setting = settings[key];
|
const setting = settings[key];
|
||||||
const { enabled } = setting;
|
const { enabled } = setting;
|
||||||
return (<Checkbox
|
return (
|
||||||
key={key}
|
<Checkbox
|
||||||
{...settings[key]}
|
key={key}
|
||||||
handleChange={() => this.props.toggleSetting(key, enabled)}
|
{...settings[key]}
|
||||||
/>);
|
handleChange={() => this.props.toggleSetting(key, enabled)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div><Trans>no_settings</Trans></div>
|
<div>
|
||||||
|
<Trans>no_settings</Trans>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { settings, dashboard, t } = this.props;
|
const {
|
||||||
|
settings, dashboard, clients, t,
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PageTitle title={ t('settings') } />
|
<PageTitle title={t('settings')} />
|
||||||
{settings.processing && <Loading />}
|
{settings.processing && <Loading />}
|
||||||
{!settings.processing &&
|
{!settings.processing && (
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-12">
|
<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">
|
<div className="form">
|
||||||
{this.renderSettings(settings.settingsList)}
|
{this.renderSettings(settings.settingsList)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,6 +92,22 @@ class Settings extends Component {
|
||||||
processingTestUpstream={settings.processingTestUpstream}
|
processingTestUpstream={settings.processingTestUpstream}
|
||||||
processingSetUpstream={settings.processingSetUpstream}
|
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
|
||||||
encryption={this.props.encryption}
|
encryption={this.props.encryption}
|
||||||
setTlsConfig={this.props.setTlsConfig}
|
setTlsConfig={this.props.setTlsConfig}
|
||||||
|
@ -97,7 +123,7 @@ class Settings extends Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 1;
|
z-index: 105;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Overlay--after-open {
|
.ReactModal__Overlay--after-open {
|
||||||
|
@ -38,3 +38,9 @@
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.modal-dialog--clients {
|
||||||
|
max-width: 650px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,12 @@ import {
|
||||||
setTlsConfig,
|
setTlsConfig,
|
||||||
validateTlsConfig,
|
validateTlsConfig,
|
||||||
} from '../actions/encryption';
|
} from '../actions/encryption';
|
||||||
|
import {
|
||||||
|
addClient,
|
||||||
|
updateClient,
|
||||||
|
deleteClient,
|
||||||
|
toggleClientModal,
|
||||||
|
} from '../actions/clients';
|
||||||
import Settings from '../components/Settings';
|
import Settings from '../components/Settings';
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
|
@ -25,12 +31,14 @@ const mapStateToProps = (state) => {
|
||||||
dashboard,
|
dashboard,
|
||||||
dhcp,
|
dhcp,
|
||||||
encryption,
|
encryption,
|
||||||
|
clients,
|
||||||
} = state;
|
} = state;
|
||||||
const props = {
|
const props = {
|
||||||
settings,
|
settings,
|
||||||
dashboard,
|
dashboard,
|
||||||
dhcp,
|
dhcp,
|
||||||
encryption,
|
encryption,
|
||||||
|
clients,
|
||||||
};
|
};
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
|
@ -50,6 +58,10 @@ const mapDispatchToProps = {
|
||||||
getTlsStatus,
|
getTlsStatus,
|
||||||
setTlsConfig,
|
setTlsConfig,
|
||||||
validateTlsConfig,
|
validateTlsConfig,
|
||||||
|
addClient,
|
||||||
|
updateClient,
|
||||||
|
deleteClient,
|
||||||
|
toggleClientModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
|
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
|
||||||
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
|
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
|
||||||
|
export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]+[:]){5})([a-fA-F0-9][a-fA-F0-9])$)|(^([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]+[.]){2}([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]))$/g;
|
||||||
|
|
||||||
export const STATS_NAMES = {
|
export const STATS_NAMES = {
|
||||||
avg_processing_time: 'average_processing_time',
|
avg_processing_time: 'average_processing_time',
|
||||||
|
@ -19,7 +20,8 @@ export const STATUS_COLORS = {
|
||||||
|
|
||||||
export const REPOSITORY = {
|
export const REPOSITORY = {
|
||||||
URL: 'https://github.com/AdguardTeam/AdGuardHome',
|
URL: 'https://github.com/AdguardTeam/AdGuardHome',
|
||||||
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
TRACKERS_DB:
|
||||||
|
'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
||||||
|
@ -165,3 +167,13 @@ export const DHCP_STATUS_RESPONSE = {
|
||||||
NO: 'no',
|
NO: 'no',
|
||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MODAL_TYPE = {
|
||||||
|
ADD: 'add',
|
||||||
|
EDIT: 'edit',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CLIENT_ID = {
|
||||||
|
MAC: 'mac',
|
||||||
|
IP: 'ip',
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
|
import { R_IPV4, R_MAC, UNSAFE_PORTS } from '../helpers/constants';
|
||||||
|
|
||||||
export const renderField = ({
|
export const renderField = ({
|
||||||
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
input, id, className, placeholder, type, disabled, meta: { touched, error },
|
||||||
|
@ -55,6 +55,13 @@ export const ipv4 = (value) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mac = (value) => {
|
||||||
|
if (value && !new RegExp(R_MAC).test(value)) {
|
||||||
|
return <Trans>form_error_mac_format</Trans>;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const isPositive = (value) => {
|
export const isPositive = (value) => {
|
||||||
if ((value || value === 0) && (value <= 0)) {
|
if ((value || value === 0) && (value <= 0)) {
|
||||||
return <Trans>form_error_positive</Trans>;
|
return <Trans>form_error_positive</Trans>;
|
||||||
|
|
|
@ -208,3 +208,21 @@ export const getClientName = (clients, ip) => {
|
||||||
const client = clients.find(item => ip === item.ip);
|
const client = clients.find(item => ip === item.ip);
|
||||||
return (client && client.name) || '';
|
return (client && client.name) || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sortClients = (clients) => {
|
||||||
|
const compare = (a, b) => {
|
||||||
|
const nameA = a.name.toUpperCase();
|
||||||
|
const nameB = b.name.toUpperCase();
|
||||||
|
let comparison = 0;
|
||||||
|
|
||||||
|
if (nameA > nameB) {
|
||||||
|
comparison = 1;
|
||||||
|
} else if (nameA < nameB) {
|
||||||
|
comparison = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return comparison;
|
||||||
|
};
|
||||||
|
|
||||||
|
return clients.sort(compare);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
|
import * as actions from '../actions/clients';
|
||||||
|
|
||||||
|
const clients = handleActions({
|
||||||
|
[actions.addClientRequest]: state => ({ ...state, processingAdding: true }),
|
||||||
|
[actions.addClientFailure]: state => ({ ...state, processingAdding: false }),
|
||||||
|
[actions.addClientSuccess]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
processingAdding: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.deleteClientRequest]: state => ({ ...state, processingDeleting: true }),
|
||||||
|
[actions.deleteClientFailure]: state => ({ ...state, processingDeleting: false }),
|
||||||
|
[actions.deleteClientSuccess]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
processingDeleting: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.updateClientRequest]: state => ({ ...state, processingUpdating: true }),
|
||||||
|
[actions.updateClientFailure]: state => ({ ...state, processingUpdating: false }),
|
||||||
|
[actions.updateClientSuccess]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
processingUpdating: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.toggleClientModal]: (state, { payload }) => {
|
||||||
|
if (payload) {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
modalType: payload.type || '',
|
||||||
|
modalClientName: payload.name || '',
|
||||||
|
isModalOpen: !state.isModalOpen,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
isModalOpen: !state.isModalOpen,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
processing: true,
|
||||||
|
processingAdding: false,
|
||||||
|
processingDeleting: false,
|
||||||
|
processingUpdating: false,
|
||||||
|
isModalOpen: false,
|
||||||
|
modalClientName: '',
|
||||||
|
modalType: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default clients;
|
|
@ -7,6 +7,7 @@ import versionCompare from '../helpers/versionCompare';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import toasts from './toasts';
|
import toasts from './toasts';
|
||||||
import encryption from './encryption';
|
import encryption from './encryption';
|
||||||
|
import clients from './clients';
|
||||||
|
|
||||||
const settings = handleActions({
|
const settings = handleActions({
|
||||||
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
|
||||||
|
@ -209,6 +210,7 @@ const dashboard = handleActions({
|
||||||
dnsAddresses: [],
|
dnsAddresses: [],
|
||||||
dnsVersion: '',
|
dnsVersion: '',
|
||||||
clients: [],
|
clients: [],
|
||||||
|
topStats: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryLogs = handleActions({
|
const queryLogs = handleActions({
|
||||||
|
@ -361,6 +363,7 @@ export default combineReducers({
|
||||||
toasts,
|
toasts,
|
||||||
dhcp,
|
dhcp,
|
||||||
encryption,
|
encryption,
|
||||||
|
clients,
|
||||||
loadingBar: loadingBarReducer,
|
loadingBar: loadingBarReducer,
|
||||||
form: formReducer,
|
form: formReducer,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue