diff --git a/client/src/__tests__/helpers.test.js b/client/src/__tests__/helpers.test.js index f974cca6..bb371be4 100644 --- a/client/src/__tests__/helpers.test.js +++ b/client/src/__tests__/helpers.test.js @@ -1,5 +1,7 @@ -import { getIpMatchListStatus, sortIp } from '../helpers/helpers'; -import { IP_MATCH_LIST_STATUS } from '../helpers/constants'; +import { + countClientsStatistics, findAddressType, getIpMatchListStatus, sortIp, +} from '../helpers/helpers'; +import { ADDRESS_TYPES, IP_MATCH_LIST_STATUS } from '../helpers/constants'; describe('getIpMatchListStatus', () => { describe('IPv4', () => { @@ -482,3 +484,56 @@ describe('sortIp', () => { }); }); }); + +describe('findAddressType', () => { + describe('ip', () => { + expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP); + }); + describe('cidr', () => { + expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR); + }); + describe('mac', () => { + expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN); + }); +}); + +describe('countClientsStatistics', () => { + test('single ip', () => { + expect(countClientsStatistics(['127.0.0.1'], { + '127.0.0.1': 1, + })).toStrictEqual(1); + }); + test('multiple ip', () => { + expect(countClientsStatistics(['127.0.0.1', '127.0.0.2'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr', () => { + expect(countClientsStatistics(['127.0.0.0/8'], { + '127.0.0.1': 1, + '127.0.0.2': 2, + })).toStrictEqual(1 + 2); + }); + test('cidr and multiple ip', () => { + expect(countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(1 + 2 + 3); + }); + test('mac', () => { + expect(countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(2 + 3); + }); + test('not found', () => { + expect(countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], { + '1.1.1.1': 1, + '2.2.2.2': 2, + '3.3.3.3': 3, + })).toStrictEqual(0); + }); +}); diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index cb7f2914..ac613164 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next'; import ReactTable from 'react-table'; import { MODAL_TYPE } from '../../../helpers/constants'; -import { splitByNewLine } from '../../../helpers/helpers'; +import { splitByNewLine, countClientsStatistics } from '../../../helpers/helpers'; import Card from '../../ui/Card'; import Modal from './Modal'; import CellWrap from '../../ui/CellWrap'; @@ -204,7 +204,10 @@ class ClientsTable extends Component { { Header: this.props.t('requests_count'), id: 'statistics', - accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0, + accessor: (row) => countClientsStatistics( + row.ids, + this.props.normalizedTopClients.auto, + ), sortMethod: (a, b) => b - a, minWidth: 120, Cell: (row) => { diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 90d06a6f..deabf72a 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -548,3 +548,9 @@ export const TOAST_TIMEOUTS = { [TOAST_TYPES.ERROR]: FAILURE_TOAST_TIMEOUT, [TOAST_TYPES.NOTICE]: FAILURE_TOAST_TIMEOUT, }; + +export const ADDRESS_TYPES = { + IP: 'IP', + CIDR: 'CIDR', + UNKNOWN: 'UNKNOWN', +}; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index bafd230e..f509ef44 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -14,6 +14,7 @@ import queryString from 'query-string'; import { getTrackerData } from './trackers/trackers'; import { + ADDRESS_TYPES, CHECK_TIMEOUT, CUSTOM_FILTERING_RULES_ID, DEFAULT_DATE_FORMAT_OPTIONS, @@ -509,6 +510,18 @@ const isIpMatchCidr = (parsedIp, parsedCidr) => { } }; +export const isIpInCidr = (ip, cidr) => { + try { + const parsedIp = ipaddr.parse(ip); + const parsedCidr = ipaddr.parseCIDR(cidr); + + return isIpMatchCidr(parsedIp, parsedCidr); + } catch (e) { + console.error(e); + return false; + } +}; + /** * The purpose of this method is to quickly check * if this IP can possibly be in the specified CIDR range. @@ -578,6 +591,29 @@ const isIpQuickMatchCIDR = (ip, listItem) => { return false; }; +/** + * + * @param ipOrCidr + * @returns {'IP' | 'CIDR' | 'UNKNOWN'} + * + */ +export const findAddressType = (address) => { + try { + const cidrMaybe = address.includes('/'); + + if (!cidrMaybe && ipaddr.isValid(address)) { + return ADDRESS_TYPES.IP; + } + if (cidrMaybe && ipaddr.parseCIDR(address)) { + return ADDRESS_TYPES.CIDR; + } + + return ADDRESS_TYPES.UNKNOWN; + } catch (e) { + return ADDRESS_TYPES.UNKNOWN; + } +}; + /** * @param ip {string} * @param list {string} @@ -622,6 +658,42 @@ export const getIpMatchListStatus = (ip, list) => { } }; +/** + * @param ids {string[]} + * @returns {Object} + */ +export const separateIpsAndCidrs = (ids) => ids.reduce((acc, curr) => { + const addressType = findAddressType(curr); + + if (addressType === ADDRESS_TYPES.IP) { + acc.ips.push(curr); + } + if (addressType === ADDRESS_TYPES.CIDR) { + acc.cidrs.push(curr); + } + return acc; +}, { ips: [], cidrs: [] }); + +export const countClientsStatistics = (ids, autoClients) => { + const { ips, cidrs } = separateIpsAndCidrs(ids); + + const ipsCount = ips.reduce((acc, curr) => { + const count = autoClients[curr] || 0; + return acc + count; + }, 0); + + const cidrsCount = Object.entries(autoClients) + .reduce((acc, curr) => { + const [id, count] = curr; + if (cidrs.some((cidr) => isIpInCidr(id, cidr))) { + // eslint-disable-next-line no-param-reassign + acc += count; + } + return acc; + }, 0); + + return ipsCount + cidrsCount; +}; /** * @param {string} elapsedMs