Merge: + clients: support per-client tags

Close #1081

* commit '67956597be3bab44124277b7a0e23c02a49da983':
  + client: handle client tags
  * minor
  + clients: support per-client tags
This commit is contained in:
Simon Zolin 2020-01-28 14:18:17 +03:00
commit 106a7a6017
19 changed files with 574 additions and 52 deletions

View File

@ -673,6 +673,7 @@ Response:
{ {
name: "client1" name: "client1"
ids: ["...", ...] // IP, CIDR or MAC ids: ["...", ...] // IP, CIDR or MAC
tags: ["...", ...]
use_global_settings: true use_global_settings: true
filtering_enabled: false filtering_enabled: false
parental_enabled: false parental_enabled: false
@ -698,6 +699,7 @@ Response:
} }
} }
] ]
supported_tags: ["...", ...]
} }
Supported keys for `whois_info`: orgname, country, city. Supported keys for `whois_info`: orgname, country, city.
@ -712,6 +714,7 @@ Request:
{ {
name: "client1" name: "client1"
ids: ["...", ...] // IP, CIDR or MAC ids: ["...", ...] // IP, CIDR or MAC
tags: ["...", ...]
use_global_settings: true use_global_settings: true
filtering_enabled: false filtering_enabled: false
parental_enabled: false parental_enabled: false
@ -742,6 +745,7 @@ Request:
data: { data: {
name: "client1" name: "client1"
ids: ["...", ...] // IP, CIDR or MAC ids: ["...", ...] // IP, CIDR or MAC
tags: ["...", ...]
use_global_settings: true use_global_settings: true
filtering_enabled: false filtering_enabled: false
parental_enabled: false parental_enabled: false

371
client/package-lock.json generated vendored
View File

@ -239,6 +239,31 @@
"@babel/types": "7.0.0-beta.44" "@babel/types": "7.0.0-beta.44"
} }
}, },
"@babel/helper-module-imports": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz",
"integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==",
"requires": {
"@babel/types": "^7.8.3"
},
"dependencies": {
"@babel/types": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz",
"integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==",
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}
}
},
"@babel/helper-split-export-declaration": { "@babel/helper-split-export-declaration": {
"version": "7.0.0-beta.44", "version": "7.0.0-beta.44",
"resolved": "http://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz", "resolved": "http://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz",
@ -518,6 +543,102 @@
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==",
"dev": true "dev": true
}, },
"@emotion/cache": {
"version": "10.0.27",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.27.tgz",
"integrity": "sha512-Zp8BEpbMunFsTcqAK4D7YTm3MvCp1SekflSLJH8lze2fCcSZ/yMkXHo8kb3t1/1Tdd3hAqf3Fb7z9VZ+FMiC9w==",
"requires": {
"@emotion/sheet": "0.9.4",
"@emotion/stylis": "0.8.5",
"@emotion/utils": "0.11.3",
"@emotion/weak-memoize": "0.2.5"
}
},
"@emotion/core": {
"version": "10.0.27",
"resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.27.tgz",
"integrity": "sha512-XbD5R36pVbohQMnKfajHv43g8EbN4NHdF6Zh9zg/C0nr0jqwOw3gYnC07Xj3yG43OYSRyrGsoQ5qPwc8ycvLZw==",
"requires": {
"@babel/runtime": "^7.5.5",
"@emotion/cache": "^10.0.27",
"@emotion/css": "^10.0.27",
"@emotion/serialize": "^0.11.15",
"@emotion/sheet": "0.9.4",
"@emotion/utils": "0.11.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz",
"integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
}
}
},
"@emotion/css": {
"version": "10.0.27",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz",
"integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==",
"requires": {
"@emotion/serialize": "^0.11.15",
"@emotion/utils": "0.11.3",
"babel-plugin-emotion": "^10.0.27"
}
},
"@emotion/hash": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.4.tgz",
"integrity": "sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A=="
},
"@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
},
"@emotion/serialize": {
"version": "0.11.15",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.15.tgz",
"integrity": "sha512-YE+qnrmGwyR+XB5j7Bi+0GT1JWsdcjM/d4POu+TXkcnrRs4RFCCsi3d/Ebf+wSStHqAlTT2+dfd+b9N9EO2KBg==",
"requires": {
"@emotion/hash": "0.7.4",
"@emotion/memoize": "0.7.4",
"@emotion/unitless": "0.7.5",
"@emotion/utils": "0.11.3",
"csstype": "^2.5.7"
}
},
"@emotion/sheet": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz",
"integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA=="
},
"@emotion/stylis": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
},
"@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"@emotion/utils": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz",
"integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw=="
},
"@emotion/weak-memoize": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
},
"@mrmlnc/readdir-enhanced": { "@mrmlnc/readdir-enhanced": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -608,6 +729,11 @@
"integrity": "sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==", "integrity": "sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==",
"dev": true "dev": true
}, },
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/q": { "@types/q": {
"version": "1.5.2", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
@ -752,7 +878,6 @@
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": { "requires": {
"color-convert": "^1.9.0" "color-convert": "^1.9.0"
} }
@ -1311,6 +1436,123 @@
"babel-runtime": "^6.22.0" "babel-runtime": "^6.22.0"
} }
}, },
"babel-plugin-emotion": {
"version": "10.0.27",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.27.tgz",
"integrity": "sha512-SUNYcT4FqhOqvwv0z1oeYhqgheU8qrceLojuHyX17ngo7WtWqN5I9l3IGHzf21Xraj465CVzF4IvOlAF+3ed0A==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@emotion/hash": "0.7.4",
"@emotion/memoize": "0.7.4",
"@emotion/serialize": "^0.11.15",
"babel-plugin-macros": "^2.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^1.0.5",
"find-root": "^1.1.0",
"source-map": "^0.5.7"
},
"dependencies": {
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
}
}
},
"babel-plugin-macros": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
"integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==",
"requires": {
"@babel/runtime": "^7.7.2",
"cosmiconfig": "^6.0.0",
"resolve": "^1.12.0"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
"integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
"requires": {
"@babel/highlight": "^7.8.3"
}
},
"@babel/highlight": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
"integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
"requires": {
"chalk": "^2.0.0",
"esutils": "^2.0.2",
"js-tokens": "^4.0.0"
}
},
"@babel/runtime": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz",
"integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"cosmiconfig": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
"integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
"requires": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.1.0",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.7.2"
}
},
"import-fresh": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
"integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
}
},
"parse-json": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
"integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
"requires": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1",
"lines-and-columns": "^1.1.6"
}
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
},
"resolve": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz",
"integrity": "sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==",
"requires": {
"path-parse": "^1.0.6"
}
},
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
}
}
},
"babel-plugin-syntax-async-functions": { "babel-plugin-syntax-async-functions": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", "resolved": "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
@ -1356,8 +1598,7 @@
"babel-plugin-syntax-jsx": { "babel-plugin-syntax-jsx": {
"version": "6.18.0", "version": "6.18.0",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "resolved": "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
"dev": true
}, },
"babel-plugin-syntax-object-rest-spread": { "babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0", "version": "6.13.0",
@ -2441,7 +2682,6 @@
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
"dev": true,
"requires": { "requires": {
"ansi-styles": "^3.2.1", "ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
@ -2985,7 +3225,6 @@
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": { "requires": {
"color-name": "1.1.3" "color-name": "1.1.3"
} }
@ -2993,8 +3232,7 @@
"color-name": { "color-name": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
"dev": true
}, },
"commander": { "commander": {
"version": "2.17.1", "version": "2.17.1",
@ -3122,8 +3360,7 @@
"convert-source-map": { "convert-source-map": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz",
"integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU="
"dev": true
}, },
"cookie": { "cookie": {
"version": "0.3.1", "version": "0.3.1",
@ -3454,6 +3691,11 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true "dev": true
}, },
"csstype": {
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz",
"integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA=="
},
"currently-unhandled": { "currently-unhandled": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@ -4034,7 +4276,6 @@
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"requires": { "requires": {
"is-arrayish": "^0.2.1" "is-arrayish": "^0.2.1"
} }
@ -4148,8 +4389,7 @@
"escape-string-regexp": { "escape-string-regexp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
"dev": true
}, },
"escope": { "escope": {
"version": "3.6.0", "version": "3.6.0",
@ -4570,8 +4810,7 @@
"esutils": { "esutils": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
"dev": true
}, },
"etag": { "etag": {
"version": "1.8.1", "version": "1.8.1",
@ -5087,6 +5326,11 @@
"pkg-dir": "^2.0.0" "pkg-dir": "^2.0.0"
} }
}, },
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": { "find-up": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
@ -5922,8 +6166,7 @@
"has-flag": { "has-flag": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
"dev": true
}, },
"has-value": { "has-value": {
"version": "1.0.0", "version": "1.0.0",
@ -6910,8 +7153,7 @@
"is-arrayish": { "is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
"dev": true
}, },
"is-binary-path": { "is-binary-path": {
"version": "1.0.1", "version": "1.0.1",
@ -7206,8 +7448,7 @@
"json-parse-better-errors": { "json-parse-better-errors": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
"dev": true
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
@ -7289,6 +7530,11 @@
"type-check": "~0.3.2" "type-check": "~0.3.2"
} }
}, },
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
},
"load-json-file": { "load-json-file": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
@ -7593,6 +7839,11 @@
"mimic-fn": "^1.0.0" "mimic-fn": "^1.0.0"
} }
}, },
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"memory-fs": { "memory-fs": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -8292,6 +8543,21 @@
"no-case": "^2.2.0" "no-case": "^2.2.0"
} }
}, },
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"requires": {
"callsites": "^3.0.0"
},
"dependencies": {
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
}
}
},
"parse-asn1": { "parse-asn1": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
@ -8380,8 +8646,7 @@
"path-parse": { "path-parse": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
"dev": true
}, },
"path-to-regexp": { "path-to-regexp": {
"version": "1.7.0", "version": "1.7.0",
@ -10082,6 +10347,14 @@
} }
} }
}, },
"react-input-autosize": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz",
"integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-is": { "react-is": {
"version": "16.6.3", "version": "16.6.3",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz",
@ -10200,6 +10473,36 @@
"prop-types": "^15.6.0" "prop-types": "^15.6.0"
} }
}, },
"react-select": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-3.0.8.tgz",
"integrity": "sha512-v9LpOhckLlRmXN5A6/mGGEft4FMrfaBFTGAnuPHcUgVId7Je42kTq9y0Z+Ye5z8/j0XDT3zUqza8gaRaI1PZIg==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/cache": "^10.0.9",
"@emotion/core": "^10.0.9",
"@emotion/css": "^10.0.9",
"memoize-one": "^5.0.0",
"prop-types": "^15.6.0",
"react-input-autosize": "^2.2.2",
"react-transition-group": "^2.2.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz",
"integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
}
}
},
"react-table": { "react-table": {
"version": "6.10.3", "version": "6.10.3",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.10.3.tgz", "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.10.3.tgz",
@ -12149,7 +12452,6 @@
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": { "requires": {
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
} }
@ -13617,6 +13919,29 @@
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
"dev": true "dev": true
}, },
"yaml": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.7.2.tgz",
"integrity": "sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw==",
"requires": {
"@babel/runtime": "^7.6.3"
},
"dependencies": {
"@babel/runtime": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz",
"integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
}
}
},
"yargs-parser": { "yargs-parser": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",

1
client/package.json vendored
View File

@ -27,6 +27,7 @@
"react-redux-loading-bar": "^4.0.7", "react-redux-loading-bar": "^4.0.7",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-router-hash-link": "^1.2.2", "react-router-hash-link": "^1.2.2",
"react-select": "^3.0.8",
"react-table": "^6.10.3", "react-table": "^6.10.3",
"react-transition-group": "^2.4.0", "react-transition-group": "^2.4.0",
"redux": "^4.0.0", "redux": "^4.0.0",

View File

@ -327,7 +327,7 @@
"client_edit": "Edit Client", "client_edit": "Edit Client",
"client_identifier": "Identifier", "client_identifier": "Identifier",
"ip_address": "IP address", "ip_address": "IP address",
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address. Please note, that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>", "client_identifier_desc": "Clients can be identified by the IP address, CIDR, 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_ip": "Enter IP",
"form_enter_mac": "Enter MAC", "form_enter_mac": "Enter MAC",
"form_enter_id": "Enter identifier", "form_enter_id": "Enter identifier",
@ -443,5 +443,8 @@
"disable_ipv6_desc": "If this feature is enabled, all DNS queries for IPv6 addresses (type AAAA) will be dropped.", "disable_ipv6_desc": "If this feature is enabled, all DNS queries for IPv6 addresses (type AAAA) will be dropped.",
"autofix_warning_text": "If you click \"Fix\", AdGuardHome will configure your system to use AdGuardHome DNS server.", "autofix_warning_text": "If you click \"Fix\", AdGuardHome will configure your system to use AdGuardHome DNS server.",
"autofix_warning_list": "It will perform these tasks: <0>Deactivate system DNSStubListener</0> <0>Set DNS server address to 127.0.0.1</0> <0>Replace symbolic link target of /etc/resolv.conf to /run/systemd/resolve/resolv.conf</0> <0>Stop DNSStubListener (reload systemd-resolved service)</0>", "autofix_warning_list": "It will perform these tasks: <0>Deactivate system DNSStubListener</0> <0>Set DNS server address to 127.0.0.1</0> <0>Replace symbolic link target of /etc/resolv.conf to /run/systemd/resolve/resolv.conf</0> <0>Stop DNSStubListener (reload systemd-resolved service)</0>",
"autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuardHome by default." "autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuardHome by default.",
"tags_title": "Tags",
"tags_desc": "You can select the tags that correspond to the client. Tags can be included in the filtering rules and allow you to apply them more accurately. <0>Learn more</0>",
"form_select_tags": "Select client tags"
} }

View File

@ -208,6 +208,7 @@ export const getClients = () => async (dispatch) => {
dispatch(getClientsSuccess({ dispatch(getClientsSuccess({
clients: sortedClients || [], clients: sortedClients || [],
autoClients: sortedAutoClients || [], autoClients: sortedAutoClients || [],
supportedTags: data.supported_tags || [],
})); }));
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));

View File

@ -33,6 +33,10 @@ class ClientsTable extends Component {
} else { } else {
config.upstreams = []; config.upstreams = [];
} }
if (values.tags) {
config.tags = values.tags.map(tag => tag.value);
}
} }
if (this.props.modalType === MODAL_TYPE.EDIT) { if (this.props.modalType === MODAL_TYPE.EDIT) {
@ -40,22 +44,29 @@ class ClientsTable extends Component {
} else { } else {
this.handleFormAdd(config); this.handleFormAdd(config);
} }
this.props.getStats();
}; };
getOptionsWithLabels = options => (
options.map(option => ({ value: option, label: option }))
);
getClient = (name, clients) => { getClient = (name, clients) => {
const client = clients.find(item => name === item.name); const client = clients.find(item => name === item.name);
if (client) { if (client) {
const { upstreams, whois_info, ...values } = client; const {
upstreams, tags, whois_info, ...values
} = client;
return { return {
upstreams: (upstreams && upstreams.join('\n')) || '', upstreams: (upstreams && upstreams.join('\n')) || '',
tags: (tags && this.getOptionsWithLabels(tags)) || [],
...values, ...values,
}; };
} }
return { return {
ids: [''], ids: [''],
tags: [],
use_global_settings: true, use_global_settings: true,
use_global_blocked_services: true, use_global_blocked_services: true,
}; };
@ -160,6 +171,30 @@ class ClientsTable extends Component {
); );
}, },
}, },
{
Header: this.props.t('tags_title'),
accessor: 'tags',
minWidth: 140,
Cell: (row) => {
const { value } = row;
if (!value || value.length < 1) {
return '';
}
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text">
{value.map(tag => (
<div key={tag} title={tag} className="small">
{tag}
</div>
))}
</span>
</div>
);
},
},
{ {
Header: this.props.t('requests_count'), Header: this.props.t('requests_count'),
id: 'statistics', id: 'statistics',
@ -223,9 +258,11 @@ class ClientsTable extends Component {
toggleClientModal, toggleClientModal,
processingAdding, processingAdding,
processingUpdating, processingUpdating,
supportedTags,
} = this.props; } = this.props;
const currentClientData = this.getClient(modalClientName, clients); const currentClientData = this.getClient(modalClientName, clients);
const tagsOptions = this.getOptionsWithLabels(supportedTags);
return ( return (
<Card <Card
@ -272,6 +309,7 @@ class ClientsTable extends Component {
handleSubmit={this.handleSubmit} handleSubmit={this.handleSubmit}
processingAdding={processingAdding} processingAdding={processingAdding}
processingUpdating={processingUpdating} processingUpdating={processingUpdating}
tagsOptions={tagsOptions}
/> />
</Fragment> </Fragment>
</Card> </Card>
@ -294,6 +332,7 @@ ClientsTable.propTypes = {
processingDeleting: PropTypes.bool.isRequired, processingDeleting: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired,
getStats: PropTypes.func.isRequired, getStats: PropTypes.func.isRequired,
supportedTags: PropTypes.array.isRequired,
}; };
export default withNamespaces()(ClientsTable); export default withNamespaces()(ClientsTable);

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form'; import { Field, FieldArray, reduxForm, formValueSelector } from 'redux-form';
import { Trans, withNamespaces } from 'react-i18next'; import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import Select from 'react-select';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs'; import Tabs from '../../ui/Tabs';
@ -99,6 +100,23 @@ const renderFieldsWrapper = (placeholder, buttonTitle) =>
// Should create function outside of component to prevent component re-renders // Should create function outside of component to prevent component re-renders
const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id')); const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id'));
const renderMultiselect = (props) => {
const { input, placeholder, options } = props;
return (
<Select
{...input}
options={options}
className="basic-multi-select"
classNamePrefix="select"
onChange={value => input.onChange(value)}
onBlur={() => input.onBlur(input.value)}
placeholder={placeholder}
isMulti
/>
);
};
let Form = (props) => { let Form = (props) => {
const { const {
t, t,
@ -113,6 +131,7 @@ let Form = (props) => {
processingAdding, processingAdding,
processingUpdating, processingUpdating,
invalid, invalid,
tagsOptions,
} = props; } = props;
return ( return (
@ -131,6 +150,27 @@ let Form = (props) => {
/> />
</div> </div>
<div className="form__group mb-4">
<div className="form__label">
<strong className="mr-3">
<Trans>tags_title</Trans>
</strong>
</div>
<div className="form__desc mt-0 mb-2">
<Trans components={[
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#ctag" key="0">link</a>,
]}>
tags_desc
</Trans>
</div>
<Field
name="tags"
component={renderMultiselect}
placeholder={t('form_select_tags')}
options={tagsOptions}
/>
</div>
<div className="form__group"> <div className="form__group">
<div className="form__label"> <div className="form__label">
<strong className="mr-3"> <strong className="mr-3">
@ -286,6 +326,7 @@ Form.propTypes = {
processingAdding: PropTypes.bool.isRequired, processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired,
invalid: PropTypes.bool.isRequired, invalid: PropTypes.bool.isRequired,
tagsOptions: PropTypes.array.isRequired,
}; };
const selector = formValueSelector('clientForm'); const selector = formValueSelector('clientForm');

View File

@ -33,6 +33,7 @@ const Modal = (props) => {
toggleClientModal, toggleClientModal,
processingAdding, processingAdding,
processingUpdating, processingUpdating,
tagsOptions,
} = props; } = props;
const initialData = getInitialData(currentClientData); const initialData = getInitialData(currentClientData);
@ -62,6 +63,7 @@ const Modal = (props) => {
toggleClientModal={toggleClientModal} toggleClientModal={toggleClientModal}
processingAdding={processingAdding} processingAdding={processingAdding}
processingUpdating={processingUpdating} processingUpdating={processingUpdating}
tagsOptions={tagsOptions}
/> />
</div> </div>
</ReactModal> </ReactModal>
@ -76,6 +78,7 @@ Modal.propTypes = {
toggleClientModal: PropTypes.func.isRequired, toggleClientModal: PropTypes.func.isRequired,
processingAdding: PropTypes.bool.isRequired, processingAdding: PropTypes.bool.isRequired,
processingUpdating: PropTypes.bool.isRequired, processingUpdating: PropTypes.bool.isRequired,
tagsOptions: PropTypes.array.isRequired,
}; };
export default withNamespaces()(Modal); export default withNamespaces()(Modal);

View File

@ -46,6 +46,7 @@ class Clients extends Component {
processingDeleting={clients.processingDeleting} processingDeleting={clients.processingDeleting}
processingUpdating={clients.processingUpdating} processingUpdating={clients.processingUpdating}
getStats={getStats} getStats={getStats}
supportedTags={dashboard.supportedTags}
/> />
<AutoClients <AutoClients
autoClients={dashboard.autoClients} autoClients={dashboard.autoClients}

View File

@ -2,7 +2,7 @@
height: 45px; height: 45px;
padding: 0 32px 2px 33px; padding: 0 32px 2px 33px;
outline: 0; outline: 0;
border-color: rgba(0, 40, 100, 0.12); border-color: rgba(69, 79, 94, 0.12);
background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg"); background-image: url("./svg/globe.svg"), url("./svg/chevron-down.svg");
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
background-position: left 11px center, right 9px center; background-position: left 11px center, right 9px center;
@ -14,3 +14,26 @@
.select--language::-ms-expand { .select--language::-ms-expand {
opacity: 0; opacity: 0;
} }
.basic-multi-select .select__control {
border: 1px solid rgba(0, 40, 100, 0.12);
border-radius: 3px;
}
.basic-multi-select .select__control:hover {
border: 1px solid rgba(0, 40, 100, 0.12);
}
.basic-multi-select .select__control--is-focused,
.basic-multi-select .select__control--is-focused:hover {
border-color: #1991eb;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
}
.basic-multi-select .select__placeholder {
color: #adb5bd;
}
.basic-multi-select .select__menu {
z-index: 3;
}

View File

@ -153,8 +153,7 @@ const dashboard = handleActions(
[actions.getClientsSuccess]: (state, { payload }) => { [actions.getClientsSuccess]: (state, { payload }) => {
const newState = { const newState = {
...state, ...state,
clients: payload.clients, ...payload,
autoClients: payload.autoClients,
processingClients: false, processingClients: false,
}; };
return newState; return newState;
@ -205,6 +204,7 @@ const dashboard = handleActions(
dnsVersion: '', dnsVersion: '',
clients: [], clients: [],
autoClients: [], autoClients: [],
supportedTags: [],
name: '', name: '',
}, },
); );

View File

@ -31,6 +31,7 @@ type RequestFilteringSettings struct {
SafeSearchEnabled bool SafeSearchEnabled bool
SafeBrowsingEnabled bool SafeBrowsingEnabled bool
ParentalEnabled bool ParentalEnabled bool
ClientTags []string
ServicesRules []ServiceEntry ServicesRules []ServiceEntry
} }
@ -264,7 +265,7 @@ func (d *Dnsfilter) CheckHostRules(host string, qtype uint16, setts *RequestFilt
return Result{}, nil return Result{}, nil
} }
return d.matchHost(host, qtype) return d.matchHost(host, qtype, setts.ClientTags)
} }
// CheckHost tries to match the host against filtering rules, // CheckHost tries to match the host against filtering rules,
@ -286,7 +287,7 @@ func (d *Dnsfilter) CheckHost(host string, qtype uint16, setts *RequestFiltering
// try filter lists first // try filter lists first
if setts.FilteringEnabled { if setts.FilteringEnabled {
result, err = d.matchHost(host, qtype) result, err = d.matchHost(host, qtype, setts.ClientTags)
if err != nil { if err != nil {
return result, err return result, err
} }
@ -475,14 +476,14 @@ func (d *Dnsfilter) initFiltering(filters map[int]string) error {
} }
// matchHost is a low-level way to check only if hostname is filtered by rules, skipping expensive safebrowsing and parental lookups // matchHost is a low-level way to check only if hostname is filtered by rules, skipping expensive safebrowsing and parental lookups
func (d *Dnsfilter) matchHost(host string, qtype uint16) (Result, error) { func (d *Dnsfilter) matchHost(host string, qtype uint16, ctags []string) (Result, error) {
d.engineLock.RLock() d.engineLock.RLock()
defer d.engineLock.RUnlock() defer d.engineLock.RUnlock()
if d.filteringEngine == nil { if d.filteringEngine == nil {
return Result{}, nil return Result{}, nil
} }
frules, ok := d.filteringEngine.Match(host) frules, ok := d.filteringEngine.Match(host, ctags)
if !ok { if !ok {
return Result{}, nil return Result{}, nil
} }

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.13
require ( require (
github.com/AdguardTeam/dnsproxy v0.23.7 github.com/AdguardTeam/dnsproxy v0.23.7
github.com/AdguardTeam/golibs v0.3.0 github.com/AdguardTeam/golibs v0.3.0
github.com/AdguardTeam/urlfilter v0.7.2 github.com/AdguardTeam/urlfilter v0.8.1
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/etcd-io/bbolt v1.3.3 github.com/etcd-io/bbolt v1.3.3
github.com/go-test/deep v1.0.4 // indirect github.com/go-test/deep v1.0.4 // indirect

4
go.sum
View File

@ -7,8 +7,8 @@ github.com/AdguardTeam/golibs v0.3.0/go.mod h1:R3M+mAg3nWG4X4Hsag5eef/TckHFH12ZY
github.com/AdguardTeam/gomitmproxy v0.1.2/go.mod h1:Mrt/3EfiXIYY2aZ7KsLuCUJzUARD/fWJ119IfzOB13M= github.com/AdguardTeam/gomitmproxy v0.1.2/go.mod h1:Mrt/3EfiXIYY2aZ7KsLuCUJzUARD/fWJ119IfzOB13M=
github.com/AdguardTeam/urlfilter v0.7.0 h1:ffFLt4rA3GX8PJYGL3bGcT5bSxZlML5k6cKpSeN2UI8= github.com/AdguardTeam/urlfilter v0.7.0 h1:ffFLt4rA3GX8PJYGL3bGcT5bSxZlML5k6cKpSeN2UI8=
github.com/AdguardTeam/urlfilter v0.7.0/go.mod h1:GHXPzEG59ezyff22lXSQ7dicj1kFZBrH5kmZ6EvQzfk= github.com/AdguardTeam/urlfilter v0.7.0/go.mod h1:GHXPzEG59ezyff22lXSQ7dicj1kFZBrH5kmZ6EvQzfk=
github.com/AdguardTeam/urlfilter v0.7.2 h1:0XyepkVAvY1eYtNKgDRCkookfaBmROvY6VE23ebPUNc= github.com/AdguardTeam/urlfilter v0.8.1 h1:9YRQOR15DU7+k01PWAgc/Ay12jjxVqSi6P0+whFm0f4=
github.com/AdguardTeam/urlfilter v0.7.2/go.mod h1:GHXPzEG59ezyff22lXSQ7dicj1kFZBrH5kmZ6EvQzfk= github.com/AdguardTeam/urlfilter v0.8.1/go.mod h1:GHXPzEG59ezyff22lXSQ7dicj1kFZBrH5kmZ6EvQzfk=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705 h1:UUppSQnhf4Yc6xGxSkoQpPhb7RVzuv5Nb1mwJ5VId9s= github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705 h1:UUppSQnhf4Yc6xGxSkoQpPhb7RVzuv5Nb1mwJ5VId9s=

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -26,6 +27,7 @@ const (
// Client information // Client information
type Client struct { type Client struct {
IDs []string IDs []string
Tags []string
Name string Name string
UseOwnSettings bool // false: use global settings UseOwnSettings bool // false: use global settings
FilteringEnabled bool FilteringEnabled bool
@ -69,6 +71,8 @@ type clientsContainer struct {
ipHost map[string]*ClientHost // IP -> Hostname ipHost map[string]*ClientHost // IP -> Hostname
lock sync.Mutex lock sync.Mutex
allTags map[string]bool
// dhcpServer is used for looking up clients IP addresses by MAC addresses // dhcpServer is used for looking up clients IP addresses by MAC addresses
dhcpServer *dhcpd.Server dhcpServer *dhcpd.Server
@ -84,6 +88,12 @@ func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.
clients.list = make(map[string]*Client) clients.list = make(map[string]*Client)
clients.idIndex = make(map[string]*Client) clients.idIndex = make(map[string]*Client)
clients.ipHost = make(map[string]*ClientHost) clients.ipHost = make(map[string]*ClientHost)
clients.allTags = make(map[string]bool)
for _, t := range clientTags {
clients.allTags[t] = false
}
clients.dhcpServer = dhcpServer clients.dhcpServer = dhcpServer
clients.addFromConfig(objects) clients.addFromConfig(objects)
@ -96,6 +106,7 @@ func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.
type clientObject struct { type clientObject struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Tags []string `yaml:"tags"`
IDs []string `yaml:"ids"` IDs []string `yaml:"ids"`
UseGlobalSettings bool `yaml:"use_global_settings"` UseGlobalSettings bool `yaml:"use_global_settings"`
FilteringEnabled bool `yaml:"filtering_enabled"` FilteringEnabled bool `yaml:"filtering_enabled"`
@ -109,6 +120,11 @@ type clientObject struct {
Upstreams []string `yaml:"upstreams"` Upstreams []string `yaml:"upstreams"`
} }
func (clients *clientsContainer) tagKnown(tag string) bool {
_, ok := clients.allTags[tag]
return ok
}
func (clients *clientsContainer) addFromConfig(objects []clientObject) { func (clients *clientsContainer) addFromConfig(objects []clientObject) {
for _, cy := range objects { for _, cy := range objects {
cli := Client{ cli := Client{
@ -125,6 +141,16 @@ func (clients *clientsContainer) addFromConfig(objects []clientObject) {
Upstreams: cy.Upstreams, Upstreams: cy.Upstreams,
} }
for _, t := range cy.Tags {
if !clients.tagKnown(t) {
log.Debug("Clients: skipping unknown tag '%s'", t)
continue
}
cli.Tags = append(cli.Tags, t)
}
sort.Strings(cli.Tags)
_, err := clients.Add(cli) _, err := clients.Add(cli)
if err != nil { if err != nil {
log.Tracef("clientAdd: %s", err) log.Tracef("clientAdd: %s", err)
@ -146,14 +172,10 @@ func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) {
UseGlobalBlockedServices: !cli.UseOwnBlockedServices, UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
} }
cy.IDs = make([]string, len(cli.IDs)) cy.Tags = stringArrayDup(cli.Tags)
copy(cy.IDs, cli.IDs) cy.IDs = stringArrayDup(cli.IDs)
cy.BlockedServices = stringArrayDup(cli.BlockedServices)
cy.BlockedServices = make([]string, len(cli.BlockedServices)) cy.Upstreams = stringArrayDup(cli.Upstreams)
copy(cy.BlockedServices, cli.BlockedServices)
cy.Upstreams = make([]string, len(cli.Upstreams))
copy(cy.Upstreams, cli.Upstreams)
*objects = append(*objects, cy) *objects = append(*objects, cy)
} }
@ -189,12 +211,26 @@ func (clients *clientsContainer) Exists(ip string, source clientSource) bool {
return true return true
} }
func stringArrayDup(a []string) []string {
a2 := make([]string, len(a))
copy(a2, a)
return a2
}
// Find searches for a client by IP // Find searches for a client by IP
func (clients *clientsContainer) Find(ip string) (Client, bool) { func (clients *clientsContainer) Find(ip string) (Client, bool) {
clients.lock.Lock() clients.lock.Lock()
defer clients.lock.Unlock() defer clients.lock.Unlock()
return clients.findByIP(ip) c, ok := clients.findByIP(ip)
if !ok {
return Client{}, false
}
c.IDs = stringArrayDup(c.IDs)
c.Tags = stringArrayDup(c.Tags)
c.BlockedServices = stringArrayDup(c.BlockedServices)
c.Upstreams = stringArrayDup(c.Upstreams)
return c, true
} }
func upstreamArrayCopy(a []upstream.Upstream) []upstream.Upstream { func upstreamArrayCopy(a []upstream.Upstream) []upstream.Upstream {
@ -297,7 +333,7 @@ func (clients *clientsContainer) FindAutoClient(ip string) (ClientHost, bool) {
} }
// Check if Client object's fields are correct // Check if Client object's fields are correct
func (c *Client) check() error { func (clients *clientsContainer) check(c *Client) error {
if len(c.Name) == 0 { if len(c.Name) == 0 {
return fmt.Errorf("Invalid Name") return fmt.Errorf("Invalid Name")
} }
@ -326,6 +362,13 @@ func (c *Client) check() error {
return fmt.Errorf("Invalid ID: %s", id) return fmt.Errorf("Invalid ID: %s", id)
} }
for _, t := range c.Tags {
if !clients.tagKnown(t) {
return fmt.Errorf("Invalid tag: %s", t)
}
}
sort.Strings(c.Tags)
if len(c.Upstreams) != 0 { if len(c.Upstreams) != 0 {
err := dnsforward.ValidateUpstreams(c.Upstreams) err := dnsforward.ValidateUpstreams(c.Upstreams)
if err != nil { if err != nil {
@ -339,7 +382,7 @@ func (c *Client) check() error {
// Add a new client object // Add a new client object
// Return true: success; false: client exists. // Return true: success; false: client exists.
func (clients *clientsContainer) Add(c Client) (bool, error) { func (clients *clientsContainer) Add(c Client) (bool, error) {
e := c.check() e := clients.check(&c)
if e != nil { if e != nil {
return false, e return false, e
} }
@ -408,7 +451,7 @@ func arraysEqual(a, b []string) bool {
// Update a client // Update a client
func (clients *clientsContainer) Update(name string, c Client) error { func (clients *clientsContainer) Update(name string, c Client) error {
err := c.check() err := clients.check(&c)
if err != nil { if err != nil {
return err return err
} }

View File

@ -9,6 +9,7 @@ import (
type clientJSON struct { type clientJSON struct {
IDs []string `json:"ids"` IDs []string `json:"ids"`
Tags []string `json:"tags"`
Name string `json:"name"` Name string `json:"name"`
UseGlobalSettings bool `json:"use_global_settings"` UseGlobalSettings bool `json:"use_global_settings"`
FilteringEnabled bool `json:"filtering_enabled"` FilteringEnabled bool `json:"filtering_enabled"`
@ -33,6 +34,7 @@ type clientHostJSON struct {
type clientListJSON struct { type clientListJSON struct {
Clients []clientJSON `json:"clients"` Clients []clientJSON `json:"clients"`
AutoClients []clientHostJSON `json:"auto_clients"` AutoClients []clientHostJSON `json:"auto_clients"`
Tags []string `json:"supported_tags"`
} }
// respond with information about configured clients // respond with information about configured clients
@ -71,6 +73,8 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
} }
clients.lock.Unlock() clients.lock.Unlock()
data.Tags = clientTags
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data) e := json.NewEncoder(w).Encode(data)
if e != nil { if e != nil {
@ -84,6 +88,7 @@ func jsonToClient(cj clientJSON) (*Client, error) {
c := Client{ c := Client{
Name: cj.Name, Name: cj.Name,
IDs: cj.IDs, IDs: cj.IDs,
Tags: cj.Tags,
UseOwnSettings: !cj.UseGlobalSettings, UseOwnSettings: !cj.UseGlobalSettings,
FilteringEnabled: cj.FilteringEnabled, FilteringEnabled: cj.FilteringEnabled,
ParentalEnabled: cj.ParentalEnabled, ParentalEnabled: cj.ParentalEnabled,
@ -103,6 +108,7 @@ func clientToJSON(c *Client) clientJSON {
cj := clientJSON{ cj := clientJSON{
Name: c.Name, Name: c.Name,
IDs: c.IDs, IDs: c.IDs,
Tags: c.Tags,
UseGlobalSettings: !c.UseOwnSettings, UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled, FilteringEnabled: c.FilteringEnabled,
ParentalEnabled: c.ParentalEnabled, ParentalEnabled: c.ParentalEnabled,

26
home/clients_tags.go Normal file
View File

@ -0,0 +1,26 @@
package home
var clientTags = []string{
"device_audio",
"device_gameconsole",
"device_laptop",
"device_nas", // Network-attached Storage
"device_other",
"device_pc",
"device_phone",
"device_printer",
"device_tablet",
"device_tv",
"os_android",
"os_ios",
"os_linux",
"os_macos",
"os_other",
"os_windows",
"user_admin",
"user_child",
"user_other",
"user_regular",
}

View File

@ -204,6 +204,8 @@ func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteri
ApplyBlockedServices(setts, c.BlockedServices) ApplyBlockedServices(setts, c.BlockedServices)
} }
setts.ClientTags = c.Tags
if !c.UseOwnSettings { if !c.UseOwnSettings {
return return
} }

View File

@ -220,16 +220,15 @@ func assignUniqueFilterID() int64 {
func periodicallyRefreshFilters() { func periodicallyRefreshFilters() {
const maxInterval = 1 * 60 * 60 const maxInterval = 1 * 60 * 60
intval := 5 // use a dynamically increasing time interval intval := 5 // use a dynamically increasing time interval
nUpdated := 0
for { for {
isNetworkErr := false isNetworkErr := false
if config.DNS.FiltersUpdateIntervalHours != 0 && refreshStatus == 0 { if config.DNS.FiltersUpdateIntervalHours != 0 && refreshStatus == 0 {
refreshStatus = 1 refreshStatus = 1
refreshLock.Lock() refreshLock.Lock()
nUpdated, isNetworkErr = refreshFiltersIfNecessary(false) _, isNetworkErr = refreshFiltersIfNecessary(false)
refreshLock.Unlock() refreshLock.Unlock()
refreshStatus = 0 refreshStatus = 0
if nUpdated != 0 { if !isNetworkErr {
intval = maxInterval intval = maxInterval
} }
} }
@ -305,6 +304,10 @@ func refreshFiltersIfNecessary(force bool) (int, bool) {
} }
config.RUnlock() config.RUnlock()
if len(updateFilters) == 0 {
return 0, false
}
nfail := 0 nfail := 0
for i := range updateFilters { for i := range updateFilters {
uf := &updateFilters[i] uf := &updateFilters[i]