mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-24 22:15:45 +03:00
Pull request 743: + client: Query Logs Infinite Scroll
Merge in DNS/adguard-home from feature/infinite_scroll_query_logs to master
Squashed commit of the following:
commit 4407ef2e7c055066257da791fbd65e6b0a495729
Merge: 40b74522 0a4781be
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 16:20:23 2020 +0300
Merge branch 'master' into feature/infinite_scroll_query_logs
commit 40b745225112cf8d664220ed8f484b0aa16e997c
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 15:46:27 2020 +0300
Remove dynamic translation of toasts
commit f08fa7b8c6a243f6b10e924aebccc183ce7814fd
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 13:59:53 2020 +0300
Remove renderLimitIdx, update isEntireLog
commit 0f1b02616faaa5759c0a3f6d8257117fa22094d9
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 11:11:14 2020 +0300
Rename variables
commit 0928570c689c1fa704af775382620d68893e7c1c
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 11:06:50 2020 +0300
Make query logs short polling function more expressive
commit 9e773cbd6c287a1c799fa2680f3462508462ea7a
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 1 11:06:19 2020 +0300
Fix Toast translation interface
commit f9c57033e5adc5788954cf086b2f114dd8938bcb
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 17:01:36 2020 +0300
Do not hide loader
commit b86ba48613437f5559a748ad9aa4cf79d15db082
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 16:56:34 2020 +0300
Add dynamic translation for all toasts
commit b9d1d9b447ca90a3c179e503fa5d4abd3516321e
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 16:39:29 2020 +0300
Prevent getting query logs recursion if query is not changed
commit e25189749f7912648cca4503cfa8d0ad898c4bb6
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Aug 31 10:13:20 2020 +0300
Decrease page limit to 20
commit 8b248ac5276899de838abf2dc9a69e47599cfc12
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Aug 28 18:47:12 2020 +0300
Return checkFilteredLogs
commit bf2d65c4a3dca0da6b15f632ae11042b7c8e2045
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Aug 28 18:33:51 2020 +0300
Review changes
commit 01b5250f9d9136a1f334086d3e2f00d1a928b37b
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Aug 28 15:29:59 2020 +0300
Remove checkFilteredLogs
commit 25b364c41e6a1489d930c8b3b39b1ab43723f29d
Merge: 1dc66034 2c666cbd
Author: Andrey Meshkov <am@adguard.com>
Date: Fri Aug 28 14:28:47 2020 +0300
Merge branch 'feature/infinite_scroll_query_logs' of ssh://bit.adguard.com:7999/dns/adguard-home into feature/infinite_scroll_query_logs
commit 1dc6603421cde9847e792bfe77ff6546e53fbc2a
Author: Andrey Meshkov <am@adguard.com>
Date: Fri Aug 28 14:28:01 2020 +0300
disregard maxFileScanEntries only if offset is set
commit bad741ed7f1dccf6959d43d000b8c0150f526f9e
Author: Andrey Meshkov <am@adguard.com>
Date: Fri Aug 28 11:57:45 2020 +0300
Fix search behavior when limit is specified
commit 2c666cbdde465cf17434126830dd99ceedfc4cbc
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 18:50:28 2020 +0300
Hide table ref loader during data loading
commit 8b4f7fe642ef9e87a979813dcdbd7817d64c27f9
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 18:43:24 2020 +0300
Repair search
commit 26fae1ae01a789999b8a2181d60b35663a20460a
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 17:59:27 2020 +0300
Resetting initial render index, change loader position on search
commit e2c97ae1a288438267eef9aec71b979319674a71
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Aug 27 16:02:03 2020 +0300
Change isScrolledIntoView
... and 32 more commits
This commit is contained in:
parent
0a4781be97
commit
6b61429572
74 changed files with 1449 additions and 1861 deletions
116
client/package-lock.json
generated
vendored
116
client/package-lock.json
generated
vendored
|
@ -1356,17 +1356,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
|
||||
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
||||
},
|
||||
"@hot-loader/react-dom": {
|
||||
"version": "16.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-16.13.0.tgz",
|
||||
"integrity": "sha512-lJZrmkucz2MrQJTQtJobx5MICXcfQvKihszqv655p557HPi0hMOWxrNpiHv3DWD8ugNWjtWcVWqRnFvwsHq1mQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.19.0"
|
||||
}
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
|
@ -10784,6 +10773,24 @@
|
|||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"concat-stream": "^1.5.0",
|
||||
"duplexify": "^3.4.2",
|
||||
"end-of-stream": "^1.1.0",
|
||||
"flush-write-stream": "^1.0.0",
|
||||
"from2": "^2.1.0",
|
||||
"parallel-transform": "^1.1.0",
|
||||
"pump": "^3.0.0",
|
||||
"pumpify": "^1.3.3",
|
||||
"stream-each": "^1.1.0",
|
||||
"through2": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
|
@ -12169,9 +12176,9 @@
|
|||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
||||
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
|
@ -12187,6 +12194,18 @@
|
|||
"duplexify": "^3.6.0",
|
||||
"inherits": "^2.0.3",
|
||||
"pump": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pump": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
|
||||
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
|
@ -13281,10 +13300,13 @@
|
|||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz",
|
||||
"integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==",
|
||||
"dev": true
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
|
||||
"integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"serve-index": {
|
||||
"version": "1.9.1",
|
||||
|
@ -14619,16 +14641,16 @@
|
|||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
|
||||
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
|
||||
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cacache": "^12.0.2",
|
||||
"find-cache-dir": "^2.1.0",
|
||||
"is-wsl": "^1.1.0",
|
||||
"schema-utils": "^1.0.0",
|
||||
"serialize-javascript": "^2.1.2",
|
||||
"serialize-javascript": "^4.0.0",
|
||||
"source-map": "^0.6.1",
|
||||
"terser": "^4.1.2",
|
||||
"webpack-sources": "^1.4.0",
|
||||
|
@ -14688,15 +14710,6 @@
|
|||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
|
@ -14707,24 +14720,6 @@
|
|||
"semver": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"concat-stream": "^1.5.0",
|
||||
"duplexify": "^3.4.2",
|
||||
"end-of-stream": "^1.1.0",
|
||||
"flush-write-stream": "^1.0.0",
|
||||
"from2": "^2.1.0",
|
||||
"parallel-transform": "^1.1.0",
|
||||
"pump": "^3.0.0",
|
||||
"pumpify": "^1.3.3",
|
||||
"stream-each": "^1.1.0",
|
||||
"through2": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
|
@ -14764,22 +14759,15 @@
|
|||
"find-up": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"serialize-javascript": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
|
||||
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
@ -14794,12 +14782,6 @@
|
|||
"requires": {
|
||||
"figgy-pudding": "^3.5.1"
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
1
client/package.json
vendored
1
client/package.json
vendored
|
@ -13,7 +13,6 @@
|
|||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@nivo/line": "^0.49.1",
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.2.6",
|
||||
|
|
|
@ -139,8 +139,8 @@
|
|||
"page_table_footer_text": "Страница",
|
||||
"rows_table_footer_text": "редове",
|
||||
"updated_custom_filtering_toast": "Обновени местни правила за филтриране",
|
||||
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране",
|
||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране",
|
||||
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
|
||||
"plain_dns": "Обикновен DNS",
|
||||
"source_label": "Източник",
|
||||
"found_in_known_domain_db": "Намерен в списъците с домейни.",
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Stránka",
|
||||
"rows_table_footer_text": "řádky",
|
||||
"updated_custom_filtering_toast": "Aktualizovaná vlastní pravidla filtrování",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravidlo odstraněno z vlastních pravidel filtrování",
|
||||
"rule_added_to_custom_filtering_toast": "Pravidlo přidáno do vlastních pravidel filtrování",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravidlo odstraněno z vlastních pravidel filtrování: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Pravidlo přidáno do vlastních pravidel filtrování: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrováno pomocí {{filter}}",
|
||||
"query_log_confirm_clear": "Opravdu chcete vymazat celý protokol dotazů?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Původní odezva",
|
||||
"click_to_view_queries": "Klikněte pro zobrazení dotazů",
|
||||
"port_53_faq_link": "Port 53 je často obsazen službami \"DNSStubListener\" nebo \"systemd-resolved\". Přečtěte si <0>tento návod</0> o tom, jak to vyřešit."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Side",
|
||||
"rows_table_footer_text": "rækker",
|
||||
"updated_custom_filtering_toast": "De brugerdefinerede filtreringsregler er blevet opdateret",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel fjernet fra de brugerdefinerede filtreringsregler",
|
||||
"rule_added_to_custom_filtering_toast": "Regel tilføjet til de brugerdefinerede filtreringsregler",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel fjernet fra de brugerdefinerede filtreringsregler: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regel tilføjet til de brugerdefinerede filtreringsregler: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtreret af {{filter}}",
|
||||
"query_log_confirm_clear": "Er du sikker på, at du vil rydde hele forespørgselsloggen?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Oprindeligt svar",
|
||||
"click_to_view_queries": "Klik for at se forespørgsler",
|
||||
"port_53_faq_link": "Port 53 optages ofte af \"DNSStubListener\" eller \"systemd-resolved\" tjenester. Læs <0>denne instruktion</0> om, hvordan du løser dette."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Seite",
|
||||
"rows_table_footer_text": "Reihen",
|
||||
"updated_custom_filtering_toast": "Die benutzerdefinierten Filterregeln wurden aktualisiert",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt",
|
||||
"rule_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Gefiltert nach {{filter}}",
|
||||
"query_log_confirm_clear": "Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Ursprüngliche Antwort",
|
||||
"click_to_view_queries": "Anklicken, um Abfragen anzuzeigen",
|
||||
"port_53_faq_link": "Port 53 wird oft von Diensten wie „DNSStubListener” oder „systemresolved” belegt. Bitte lesen Sie <0>diese Anweisung</0>, wie dies behoben werden kann."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -213,8 +213,8 @@
|
|||
"page_table_footer_text": "Page",
|
||||
"rows_table_footer_text": "rows",
|
||||
"updated_custom_filtering_toast": "Updated the custom filtering rules",
|
||||
"rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules",
|
||||
"rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules",
|
||||
"rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtered by {{filter}}",
|
||||
"query_log_confirm_clear": "Are you sure you want to clear the entire query log?",
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Página",
|
||||
"rows_table_footer_text": "filas",
|
||||
"updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado",
|
||||
"rule_added_to_custom_filtering_toast": "Regla añadida a las reglas de filtrado personalizado",
|
||||
"rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regla añadida a las reglas de filtrado personalizado: {{rule}}",
|
||||
"query_log_response_status": "Estado: {{value}}",
|
||||
"query_log_filtered": "Filtrado por {{filter}}",
|
||||
"query_log_confirm_clear": "¿Está seguro de que desea borrar todo el registro de consultas?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Respuesta original",
|
||||
"click_to_view_queries": "Clic para ver las consultas",
|
||||
"port_53_faq_link": "El puerto 53 suele estar ocupado por los servicios \"DNSStubListener\" o \"systemd-resolved\". Por favor lee <0>esta instrucción</0> sobre cómo resolver esto."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -202,8 +202,8 @@
|
|||
"page_table_footer_text": "صفحه",
|
||||
"rows_table_footer_text": "سطر",
|
||||
"updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است",
|
||||
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد",
|
||||
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد",
|
||||
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد {{rule}}",
|
||||
"query_log_response_status": "وضعیت: {{value}}",
|
||||
"query_log_filtered": "فیلتر شده با {{filter}}",
|
||||
"query_log_confirm_clear": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟",
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Page",
|
||||
"rows_table_footer_text": "lignes",
|
||||
"updated_custom_filtering_toast": "Règles de filtrage d'utilisateur mises à jour",
|
||||
"rule_removed_from_custom_filtering_toast": "Règle retirée des règles d'utilisateur",
|
||||
"rule_added_to_custom_filtering_toast": "Règle ajoutée aux règles d'utilisateur",
|
||||
"rule_removed_from_custom_filtering_toast": "Règle retirée des règles d'utilisateur: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Règle ajoutée aux règles d'utilisateur: {{rule}}",
|
||||
"query_log_response_status": "Statut : {{value}}",
|
||||
"query_log_filtered": "Filtré par {{filter}}",
|
||||
"query_log_confirm_clear": "Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?",
|
||||
|
@ -561,4 +561,4 @@
|
|||
"filter_category_other_desc": "Autres listes noires",
|
||||
"click_to_view_queries": "Cliquez pour voir les requêtes",
|
||||
"port_53_faq_link": "Le port 53 est souvent occupé par les services « DNSStubListener » ou « systemd-resolved ». Veuillez lire <0>cette instruction</0> pour savoir comment résoudre ce problème."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Stranica",
|
||||
"rows_table_footer_text": "redova",
|
||||
"updated_custom_filtering_toast": "Ažurirana su prilagođena pravila filtriranja",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrirao {{filter}}",
|
||||
"query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Originalni odgovor",
|
||||
"click_to_view_queries": "Kliknite za pregled upita",
|
||||
"port_53_faq_link": "Port 53 često zauzimaju usluge \"DNSStubListener\" ili \"systemd-resolved\". Molimo pročitajte <0>ove upute</0> o tome kako to riješiti."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Halaman",
|
||||
"rows_table_footer_text": "baris",
|
||||
"updated_custom_filtering_toast": "Perbarui aturan penyaringan khusus",
|
||||
"rule_removed_from_custom_filtering_toast": "Aturan dihapus dari aturan penyaringan khusus",
|
||||
"rule_added_to_custom_filtering_toast": "Aturan ditambah ke aturan penyaringan khusus",
|
||||
"rule_removed_from_custom_filtering_toast": "Aturan dihapus dari aturan penyaringan khusus: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Aturan ditambah ke aturan penyaringan khusus: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Difilter oleh {{filter}}",
|
||||
"query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Respon asli",
|
||||
"click_to_view_queries": "Klik untuk lihat permintaan",
|
||||
"port_53_faq_link": "Port 53 sering ditempati oleh layanan \"DNSStubListener\" atau \"systemd-resolved\". Silakan baca <0>instruksi ini</0> tentang cara menyelesaikan ini."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Pagina",
|
||||
"rows_table_footer_text": "righe",
|
||||
"updated_custom_filtering_toast": "Le regole dei filtri personalizzate sono state aggiornate",
|
||||
"rule_removed_from_custom_filtering_toast": "Regola rimossa dalle regole dei filtri personalizzate",
|
||||
"rule_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate",
|
||||
"rule_removed_from_custom_filtering_toast": "Regola rimossa dalle regole dei filtri personalizzate: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrato da {{filter}}",
|
||||
"query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?",
|
||||
|
@ -560,4 +560,4 @@
|
|||
"filter_category_regional_desc": "Liste focalizzare su pubblicità regionali e server traccianti",
|
||||
"filter_category_other_desc": "Altre liste di blocco",
|
||||
"click_to_view_queries": "Clicca per visualizzare query"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "ページ",
|
||||
"rows_table_footer_text": "行",
|
||||
"updated_custom_filtering_toast": "カスタム・フィルタリングルールを更新しました",
|
||||
"rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました",
|
||||
"rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました",
|
||||
"rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました {{rule}}",
|
||||
"query_log_response_status": "ステータス: {{value}}",
|
||||
"query_log_filtered": "{{filter}}によるフィルタ",
|
||||
"query_log_confirm_clear": "クエリ・ログ全体を消去してもよろしいですか?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "当初の応答",
|
||||
"click_to_view_queries": "クエリを表示するにはクリックしてください",
|
||||
"port_53_faq_link": "多くの場合、ポート53は \"DNSStubListener\" または \"systemd-resolved\" サービスによって利用されています。これを解決する方法については、<0>この手順</0>をお読みください。"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "페이지",
|
||||
"rows_table_footer_text": "행",
|
||||
"updated_custom_filtering_toast": "사용자 정의 필터링 규칙 업데이트",
|
||||
"rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거",
|
||||
"rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙",
|
||||
"rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거 {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙 {{rule}}",
|
||||
"query_log_response_status": "상태: {{value}}",
|
||||
"query_log_filtered": "필터: {{filter}}",
|
||||
"query_log_confirm_clear": "정말로 모든 쿼리 로그를 비우시겠습니까?",
|
||||
|
@ -563,4 +563,4 @@
|
|||
"filter_category_other_desc": "기타 차단 목록",
|
||||
"original_response": "원래 응답",
|
||||
"click_to_view_queries": "쿼리를 보려면 클릭합니다"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Pagina",
|
||||
"rows_table_footer_text": "rijen",
|
||||
"updated_custom_filtering_toast": "Aangepaste filter regels zijn bijgewerkt",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel verwijderd uit de aangepaste filterregels",
|
||||
"rule_added_to_custom_filtering_toast": "Regel toegevoegd aan de aangepaste filterregels",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel verwijderd uit de aangepaste filterregels: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regel toegevoegd aan de aangepaste filterregels: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Gefilterd door {{filter}}",
|
||||
"query_log_confirm_clear": "Weet u zeker dat u het hele query logboek wilt legen?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Oorspronkelijke reactie",
|
||||
"click_to_view_queries": "Klik om queries te bekijken",
|
||||
"port_53_faq_link": "Poort 53 wordt vaak gebruikt door services als DNSStubListener- of de systeem DNS-resolver. Lees a.u.b. <0>deze instructie</0> hoe dit is op te lossen."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -207,8 +207,8 @@
|
|||
"page_table_footer_text": "Side",
|
||||
"rows_table_footer_text": "rekker",
|
||||
"updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene",
|
||||
"rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene",
|
||||
"rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene",
|
||||
"rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrert av {{filter}}",
|
||||
"query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?",
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Strona",
|
||||
"rows_table_footer_text": "wierszy",
|
||||
"updated_custom_filtering_toast": "Zaktualizowano niestandardowe reguły filtrowania",
|
||||
"rule_removed_from_custom_filtering_toast": "Reguła usunięta z niestandardowych reguł filtrowania",
|
||||
"rule_added_to_custom_filtering_toast": "Reguła dodana do niestandardowych reguł filtrowania",
|
||||
"rule_removed_from_custom_filtering_toast": "Reguła usunięta z niestandardowych reguł filtrowania: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Reguła dodana do niestandardowych reguł filtrowania: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrowane przez {{filter}}",
|
||||
"query_log_confirm_clear": "Czy na pewno chcesz wyczyścić cały dziennik zapytań?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Oryginalna odpowiedź",
|
||||
"click_to_view_queries": "Kliknij, aby wyświetlić zapytania",
|
||||
"port_53_faq_link": "Port 53 jest często zajęty przez usługi \"DNSStubListener\" lub \"systemd-resolved\". Przeczytaj <0>tę instrukcję</0> jak to rozwiązać."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Página",
|
||||
"rows_table_footer_text": "linhas",
|
||||
"updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas",
|
||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrado por {{filter}}",
|
||||
"query_log_confirm_clear": "Você tem certeza que deseja limpar o registro de consulta?",
|
||||
|
@ -560,4 +560,4 @@
|
|||
"filter_category_regional_desc": "Listas focadas em anúncios regionais e servidores de rastreamento",
|
||||
"filter_category_other_desc": "Outras listas negras",
|
||||
"click_to_view_queries": "Clique para ver as consultas"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,8 +168,8 @@
|
|||
"page_table_footer_text": "Página",
|
||||
"rows_table_footer_text": "linhas",
|
||||
"updated_custom_filtering_toast": "Regras de filtragem personalizadas actualizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas",
|
||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas",
|
||||
"rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrado por {{filter}}",
|
||||
"query_log_confirm_clear": "Tem a certeza de que deseja limpar todo o registo de consulta?",
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Pagina",
|
||||
"rows_table_footer_text": "linii",
|
||||
"updated_custom_filtering_toast": "Reguli personalizate de filtrare aduse la zi",
|
||||
"rule_removed_from_custom_filtering_toast": "Regulă scoasă din regullei personalizate de filtrare",
|
||||
"rule_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate",
|
||||
"rule_removed_from_custom_filtering_toast": "Regulă scoasă din regullei personalizate de filtrare: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate: {{rule}}",
|
||||
"query_log_response_status": "Statut: {{value}}",
|
||||
"query_log_filtered": "Filtrat de {{filter}}",
|
||||
"query_log_confirm_clear": "Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Răspuns original",
|
||||
"click_to_view_queries": "Clicați pentru a vizualiza interogări",
|
||||
"port_53_faq_link": "Portul 53 este adesea ocupat de serviciile \"DNSStubListener\" sau \"systemd-resolved\". Vă rugăm să citiți <0>această instrucțiune</0> despre cum să rezolvați aceasta."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Страница",
|
||||
"rows_table_footer_text": "строк",
|
||||
"updated_custom_filtering_toast": "Внесены изменения в пользовательские правила",
|
||||
"rule_removed_from_custom_filtering_toast": "Правило удалено из авторского списка правил фильтрации",
|
||||
"rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено",
|
||||
"rule_removed_from_custom_filtering_toast": "Пользовательское правило удалено: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено: {{rule}}",
|
||||
"query_log_response_status": "Статус: {{value}}",
|
||||
"query_log_filtered": "Отфильтровано с помощью {{filter}}",
|
||||
"query_log_confirm_clear": "Вы уверены, что хотите очистить весь журнал запросов?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Первоначальный ответ",
|
||||
"click_to_view_queries": "Нажмите, чтобы просмотреть запросы",
|
||||
"port_53_faq_link": "Порт 53 часто занят службами \"DNSStubListener\" или \"systemd-resolved\". Ознакомьтесь с <0>инструкцией</0> о том, как это разрешить."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Stránka",
|
||||
"rows_table_footer_text": "riadky",
|
||||
"updated_custom_filtering_toast": "Aktualizované vlastné filtračné pravidlá",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravidlo odstránené z vlastných filtračných pravidiel",
|
||||
"rule_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravidlo odstránené z vlastných filtračných pravidiel: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel: {{rule}}",
|
||||
"query_log_response_status": "Stav: {{value}}",
|
||||
"query_log_filtered": "Vyfiltrované pomocou {{filter}}",
|
||||
"query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Pôvodná odozva",
|
||||
"click_to_view_queries": "Kliknite pre zobrazenie dopytov",
|
||||
"port_53_faq_link": "Port 53 je často obsadený službami \"DNSStubListener\" alebo \"systemd-resolved\". Prečítajte si <0>tento návod</0> o tom, ako to vyriešiť."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Stran",
|
||||
"rows_table_footer_text": "vrstic",
|
||||
"updated_custom_filtering_toast": "Posodobljena pravila filtriranja po meri",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil filtriranja po meri",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano pravilom filtriranja po meri",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil filtriranja po meri: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano pravilom filtriranja po meri: {{rule}}",
|
||||
"query_log_response_status": "Stanje: {{value}}",
|
||||
"query_log_filtered": "Filtriran z {{filter}}",
|
||||
"query_log_confirm_clear": "Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Izviren odgovor",
|
||||
"click_to_view_queries": "Kliknite za prikaz poizvedb",
|
||||
"port_53_faq_link": "Vrata 53 pogosto zasedajo storitve 'DNSStubListener' ali 'Sistemsko razrešene storitve'. Preberite <0>to navodilo</0> o tem, kako to rešiti."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "Stranica",
|
||||
"rows_table_footer_text": "redovi",
|
||||
"updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja: {{rule}}",
|
||||
"query_log_response_status": "Stanje: {{value}}",
|
||||
"query_log_filtered": "Filtrirano od {{filter}}",
|
||||
"query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "Izvorni odgovor",
|
||||
"click_to_view_queries": "Kliknite da pogledate zahteve",
|
||||
"port_53_faq_link": "Port 53 je najčešće zauzet od \"DNSStubListener\" ili \"systemd-resolved\" usluga. Pročitajte <0>ovo uputstvo</0> kako da to rešite."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,8 +162,8 @@
|
|||
"page_table_footer_text": "Sida",
|
||||
"rows_table_footer_text": "rader",
|
||||
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna",
|
||||
"rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna",
|
||||
"rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
"query_log_filtered": "Filtrerat av {{filter}}",
|
||||
"query_log_confirm_clear": "Är du säker på att du vill rensa hela förfrågningsloggen?",
|
||||
|
|
|
@ -167,8 +167,8 @@
|
|||
"page_table_footer_text": "หน้า",
|
||||
"rows_table_footer_text": "ตาราง",
|
||||
"updated_custom_filtering_toast": "อัปเดตกฎการกรองที่กำหนดเอง",
|
||||
"rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว",
|
||||
"rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว",
|
||||
"rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว {{rule}}",
|
||||
"query_log_response_status": "สถานะ: {{value}}",
|
||||
"query_log_filtered": "กรองโดย {{filter}}",
|
||||
"query_log_confirm_clear": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?",
|
||||
|
|
|
@ -197,8 +197,8 @@
|
|||
"page_table_footer_text": "Sayfa",
|
||||
"rows_table_footer_text": "satır",
|
||||
"updated_custom_filtering_toast": "İsteğe bağlı filtreleme kuralları güncellendi",
|
||||
"rule_removed_from_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarından kaldırıldı",
|
||||
"rule_added_to_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarına eklendi",
|
||||
"rule_removed_from_custom_filtering_toast": "Özel filtreleme kurallarından kural kaldırıldı: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Özel filtreleme kurallarına kural eklendi: {{rule}}",
|
||||
"query_log_response_status": "Durum: {{value}}",
|
||||
"query_log_filtered": "{{filter}} tarafından filtrelendi",
|
||||
"query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?",
|
||||
|
@ -507,4 +507,4 @@
|
|||
"allowed": "İzin verildi",
|
||||
"blocklist": "Engellenen listesi",
|
||||
"port_53_faq_link": "Port 53 genellikle \"DNSStubListener\" veya \"sistemd-resolved\" hizmetler tarafından kullanılır. Lütfen problemin nasıl çözüleceğine ilişkin <0>bu talimatı</0> okuyun."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,8 +172,8 @@
|
|||
"page_table_footer_text": "Trang",
|
||||
"rows_table_footer_text": "hàng",
|
||||
"updated_custom_filtering_toast": "Đã cập nhật quy tắc lọc tuỳ chỉnh",
|
||||
"rule_removed_from_custom_filtering_toast": "Quy tắc đã được xoá khỏi quy tắc lọc tuỳ chỉnh",
|
||||
"rule_added_to_custom_filtering_toast": "Quy tắc đã được thêm vào quy tắc lọc tuỳ chỉnh",
|
||||
"rule_removed_from_custom_filtering_toast": "Quy tắc đã được xoá khỏi quy tắc lọc tuỳ chỉnh {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Quy tắc đã được thêm vào quy tắc lọc tuỳ chỉnh: {{rule}}",
|
||||
"query_log_response_status": "Trạng thái: {{value}}",
|
||||
"query_log_filtered": "Được lọc bởi {{filter}}",
|
||||
"query_log_confirm_clear": "Bạn có chắc chắn muốn xóa toàn bộ nhật ký truy vấn không?",
|
||||
|
@ -445,4 +445,4 @@
|
|||
"blocked_threats": "Mối nguy hiểm đã chặn",
|
||||
"allowed": "Được phép",
|
||||
"safe_search": "Tìm kiếm an toàn"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "页",
|
||||
"rows_table_footer_text": "行",
|
||||
"updated_custom_filtering_toast": "自定义过滤规则已更新",
|
||||
"rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除",
|
||||
"rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中",
|
||||
"rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除 {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中 {{rule}}",
|
||||
"query_log_response_status": "状态: {{value}}",
|
||||
"query_log_filtered": "被 {{filter}} 过滤",
|
||||
"query_log_confirm_clear": "你确定想要清除全部查询日志吗?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "原始响应",
|
||||
"click_to_view_queries": "点击查看查询",
|
||||
"port_53_faq_link": "53端口常被DNSStubListener或systemdn解析的服务占用。请阅读<0>这份关于如何解决这一问题的说明</0>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@
|
|||
"page_table_footer_text": "頁面",
|
||||
"rows_table_footer_text": "列",
|
||||
"updated_custom_filtering_toast": "已更新自訂的過濾規則",
|
||||
"rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除",
|
||||
"rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中",
|
||||
"rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除 {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中 {{rule}}",
|
||||
"query_log_response_status": "狀態:{{value}}",
|
||||
"query_log_filtered": "被 {{filter}} 過濾",
|
||||
"query_log_confirm_clear": "您確定您想要清除整個查詢記錄嗎?",
|
||||
|
@ -564,4 +564,4 @@
|
|||
"original_response": "原始的回應",
|
||||
"click_to_view_queries": "點擊以檢視查詢",
|
||||
"port_53_faq_link": "連接埠 53 常被 \"DNSStubListener\" 或 \"systemd-resolved\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明</0>。"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,17 @@ import { createAction } from 'redux-actions';
|
|||
import i18next from 'i18next';
|
||||
import axios from 'axios';
|
||||
|
||||
import endsWith from 'lodash/endsWith';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import { splitByNewLine, sortClients } from '../helpers/helpers';
|
||||
import {
|
||||
CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME,
|
||||
BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME,
|
||||
} from '../helpers/constants';
|
||||
import { areEqualVersions } from '../helpers/version';
|
||||
import { getTlsStatus } from './encryption';
|
||||
import apiClient from '../api/Api';
|
||||
import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts';
|
||||
import { getFilteringStatus, setRules } from './filtering';
|
||||
|
||||
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
||||
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||
|
@ -541,3 +544,35 @@ export const removeStaticLease = (config) => async (dispatch) => {
|
|||
};
|
||||
|
||||
export const removeToast = createAction('REMOVE_TOAST');
|
||||
|
||||
export const toggleBlocking = (type, domain) => async (dispatch, getState) => {
|
||||
const { userRules } = getState().filtering;
|
||||
|
||||
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
||||
const baseRule = `||${domain}^$important`;
|
||||
const baseUnblocking = `@@${baseRule}`;
|
||||
|
||||
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule;
|
||||
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking;
|
||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||
|
||||
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
|
||||
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
|
||||
|
||||
if (matchPreparedBlockingRule) {
|
||||
dispatch(setRules(userRules.replace(`${blockingRule}`, '')));
|
||||
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||
} else if (!matchPreparedUnblockingRule) {
|
||||
dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`));
|
||||
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||
} else if (matchPreparedUnblockingRule) {
|
||||
dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule })));
|
||||
return;
|
||||
} else if (!matchPreparedBlockingRule) {
|
||||
dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule })));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(getFilteringStatus());
|
||||
};
|
||||
|
|
|
@ -3,9 +3,7 @@ import { createAction } from 'redux-actions';
|
|||
import apiClient from '../api/Api';
|
||||
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
|
||||
import {
|
||||
DEFAULT_LOGS_FILTER,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT,
|
||||
} from '../helpers/constants';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
|
||||
|
@ -37,15 +35,22 @@ export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUES
|
|||
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
|
||||
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
|
||||
|
||||
const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
||||
const shortPollQueryLogs = async (data, filter, dispatch, getState, total) => {
|
||||
const { logs, oldest } = data;
|
||||
const totalData = total || { logs };
|
||||
|
||||
const needToGetAdditionalLogs = (logs.length < TABLE_DEFAULT_PAGE_SIZE
|
||||
|| totalData.logs.length < TABLE_DEFAULT_PAGE_SIZE)
|
||||
&& oldest !== '';
|
||||
const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
|
||||
const currentQuery = queryForm && queryForm.values.search;
|
||||
const previousQuery = filter?.search;
|
||||
const isQueryTheSame = typeof previousQuery === 'string'
|
||||
&& typeof currentQuery === 'string'
|
||||
&& previousQuery === currentQuery;
|
||||
|
||||
if (needToGetAdditionalLogs) {
|
||||
const isShortPollingNeeded = (logs.length < QUERY_LOGS_PAGE_LIMIT
|
||||
|| totalData.logs.length < QUERY_LOGS_PAGE_LIMIT)
|
||||
&& oldest !== '' && isQueryTheSame;
|
||||
|
||||
if (isShortPollingNeeded) {
|
||||
dispatch(getAdditionalLogsRequest());
|
||||
|
||||
try {
|
||||
|
@ -54,7 +59,7 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
|||
filter,
|
||||
});
|
||||
if (additionalLogs.oldest.length > 0) {
|
||||
return await checkFilteredLogs(additionalLogs, filter, dispatch, {
|
||||
return await shortPollQueryLogs(additionalLogs, filter, dispatch, getState, {
|
||||
logs: [...totalData.logs, ...additionalLogs.logs],
|
||||
oldest: additionalLogs.oldest,
|
||||
});
|
||||
|
@ -71,31 +76,25 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
|
|||
return totalData;
|
||||
};
|
||||
|
||||
export const setLogsPagination = createAction('LOGS_PAGINATION');
|
||||
export const setLogsPage = createAction('SET_LOG_PAGE');
|
||||
export const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS');
|
||||
|
||||
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
|
||||
export const getLogsFailure = createAction('GET_LOGS_FAILURE');
|
||||
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
|
||||
|
||||
export const getLogs = (config) => async (dispatch, getState) => {
|
||||
export const getLogs = () => async (dispatch, getState) => {
|
||||
dispatch(getLogsRequest());
|
||||
try {
|
||||
const { isFiltered, filter, page } = getState().queryLogs;
|
||||
const { isFiltered, filter, oldest } = getState().queryLogs;
|
||||
const data = await getLogsWithParams({
|
||||
...config,
|
||||
older_than: oldest,
|
||||
filter,
|
||||
});
|
||||
|
||||
if (isFiltered) {
|
||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
||||
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||
dispatch(getLogsSuccess(updatedData));
|
||||
dispatch(setLogsPagination({
|
||||
page,
|
||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||
}));
|
||||
} else {
|
||||
dispatch(getLogsSuccess(data));
|
||||
}
|
||||
|
@ -111,7 +110,7 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
|
|||
*
|
||||
* @param filter
|
||||
* @param {string} filter.search
|
||||
* @param {string} filter.response_status query field of RESPONSE_FILTER object
|
||||
* @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object
|
||||
* @returns function
|
||||
*/
|
||||
export const setLogsFilter = (filter) => setLogsFilterRequest(filter);
|
||||
|
@ -120,21 +119,20 @@ export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST');
|
|||
export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
|
||||
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
|
||||
|
||||
export const setFilteredLogs = (filter) => async (dispatch) => {
|
||||
export const setFilteredLogs = (filter) => async (dispatch, getState) => {
|
||||
dispatch(setFilteredLogsRequest());
|
||||
try {
|
||||
const data = await getLogsWithParams({
|
||||
older_than: '',
|
||||
filter,
|
||||
});
|
||||
const additionalData = await checkFilteredLogs(data, filter, dispatch);
|
||||
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||
|
||||
dispatch(setFilteredLogsSuccess({
|
||||
...updatedData,
|
||||
filter,
|
||||
}));
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(setFilteredLogsFailure(error));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import axios from 'axios';
|
||||
|
||||
import { getPathWithQueryString } from '../helpers/helpers';
|
||||
import { R_PATH_LAST_PART } from '../helpers/constants';
|
||||
import { QUERY_LOGS_PAGE_LIMIT, R_PATH_LAST_PART } from '../helpers/constants';
|
||||
import { BASE_URL } from '../../constants';
|
||||
|
||||
class Api {
|
||||
|
@ -530,6 +530,8 @@ class Api {
|
|||
|
||||
getQueryLog(params) {
|
||||
const { path, method } = this.GET_QUERY_LOG;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
params.limit = QUERY_LOGS_PAGE_LIMIT;
|
||||
const url = getPathWithQueryString(path, params);
|
||||
return this.makeRequest(url, method);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ import Header from '../Header';
|
|||
import { changeLanguage, getDnsStatus } from '../../actions';
|
||||
|
||||
import Dashboard from '../../containers/Dashboard';
|
||||
import Logs from '../../containers/Logs';
|
||||
import SetupGuide from '../../containers/SetupGuide';
|
||||
import Settings from '../../containers/Settings';
|
||||
import Dns from '../../containers/Dns';
|
||||
|
@ -38,6 +37,7 @@ import DnsAllowlist from '../../containers/DnsAllowlist';
|
|||
import DnsRewrites from '../../containers/DnsRewrites';
|
||||
import CustomRules from '../../containers/CustomRules';
|
||||
import Services from '../Filters/Services';
|
||||
import Logs from '../Logs';
|
||||
|
||||
|
||||
const ROUTES = [
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
|
||||
import { IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
|
||||
import { formatClientCell } from '../../helpers/formatClientCell';
|
||||
import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
|
||||
import { toggleClientBlock } from '../../actions/access';
|
||||
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
|
||||
|
||||
const getClientsPercentColor = (percent) => {
|
||||
if (percent > 50) {
|
||||
|
@ -20,126 +23,131 @@ const getClientsPercentColor = (percent) => {
|
|||
return STATUS_COLORS.red;
|
||||
};
|
||||
|
||||
const countCell = (dnsQueries) => function cell(row) {
|
||||
const { value } = row;
|
||||
const percent = getPercent(dnsQueries, value);
|
||||
const CountCell = (row) => {
|
||||
const { value, original: { ip } } = row;
|
||||
const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual);
|
||||
|
||||
const percent = getPercent(numDnsQueries, value);
|
||||
const percentColor = getClientsPercentColor(percent);
|
||||
|
||||
return <Cell value={value} percent={percent} color={percentColor} search={row.original.ip} />;
|
||||
return <Cell value={value} percent={percent} color={percentColor} search={ip} />;
|
||||
};
|
||||
|
||||
const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) => {
|
||||
const buttonProps = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND
|
||||
? {
|
||||
className: 'btn-outline-danger',
|
||||
text: 'block',
|
||||
type: 'block',
|
||||
const renderBlockingButton = (ip) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const processingSet = useSelector((state) => state.access.processingSet);
|
||||
const disallowed_clients = useSelector(
|
||||
(state) => state.access.disallowed_clients, shallowEqual,
|
||||
);
|
||||
|
||||
const ipMatchListStatus = getIpMatchListStatus(ip, disallowed_clients);
|
||||
|
||||
if (ipMatchListStatus === IP_MATCH_LIST_STATUS.CIDR) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND;
|
||||
const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
|
||||
const text = type;
|
||||
|
||||
const className = classNames('btn btn-sm', {
|
||||
'btn-outline-danger': isNotFound,
|
||||
'btn-outline-secondary': !isNotFound,
|
||||
});
|
||||
|
||||
const toggleClientStatus = (type, ip) => {
|
||||
const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock';
|
||||
|
||||
if (window.confirm(t(confirmMessage, { ip }))) {
|
||||
dispatch(toggleClientBlock(type, ip));
|
||||
}
|
||||
: {
|
||||
className: 'btn-outline-secondary',
|
||||
text: 'unblock',
|
||||
type: 'unblock',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table__action button__action">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonProps.className}`}
|
||||
onClick={() => handleClick(buttonProps.type, ip)}
|
||||
disabled={processing}
|
||||
>
|
||||
<Trans>{buttonProps.text}</Trans>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
const onClick = () => toggleClientStatus(type, ip);
|
||||
|
||||
return <div className="table__action pl-4">
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
disabled={processingSet}
|
||||
>
|
||||
<Trans>{text}</Trans>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const clientCell = (t, toggleClientStatus, processing, disallowedClients) => function cell(row) {
|
||||
const { value } = row;
|
||||
const ipMatchListStatus = getIpMatchListStatus(value, disallowedClients);
|
||||
const ClientCell = (row) => {
|
||||
const { value, original: { info } } = row;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="logs__row logs__row--overflow logs__row--column">
|
||||
{formatClientCell(row, true, false)}
|
||||
</div>
|
||||
{ipMatchListStatus !== IP_MATCH_LIST_STATUS.CIDR
|
||||
&& renderBlockingButton(ipMatchListStatus, value, toggleClientStatus, processing)}
|
||||
</>
|
||||
);
|
||||
return <>
|
||||
<div className="logs__row logs__row--overflow logs__row--column d-flex">
|
||||
{renderFormattedClientCell(value, info, true)}
|
||||
{renderBlockingButton(value)}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
const Clients = ({
|
||||
t,
|
||||
refreshButton,
|
||||
topClients,
|
||||
subtitle,
|
||||
dnsQueries,
|
||||
toggleClientStatus,
|
||||
processingAccessSet,
|
||||
disallowedClients,
|
||||
}) => (
|
||||
<Card
|
||||
title={t('top_clients')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
|
||||
const disallowedClients = useSelector((state) => state.access.disallowed_clients, shallowEqual);
|
||||
|
||||
return <Card
|
||||
title={t('top_clients')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<ReactTable
|
||||
data={topClients.map(({
|
||||
name: ip, count, info, blocked,
|
||||
}) => ({
|
||||
ip,
|
||||
count,
|
||||
info,
|
||||
blocked,
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
sortMethod: sortIp,
|
||||
Cell: clientCell(t, toggleClientStatus, processingAccessSet, disallowedClients),
|
||||
},
|
||||
{
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'count',
|
||||
minWidth: 180,
|
||||
maxWidth: 200,
|
||||
Cell: countCell(dnsQueries),
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_clients_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
className="-highlight card-table-overflow--limited clients__table"
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
data={topClients.map(({
|
||||
name: ip, count, info, blocked,
|
||||
}) => ({
|
||||
ip,
|
||||
count,
|
||||
info,
|
||||
blocked,
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
Header: 'IP',
|
||||
accessor: 'ip',
|
||||
sortMethod: sortIp,
|
||||
Cell: ClientCell,
|
||||
},
|
||||
{
|
||||
Header: <Trans>requests_count</Trans>,
|
||||
accessor: 'count',
|
||||
minWidth: 180,
|
||||
maxWidth: 200,
|
||||
Cell: CountCell,
|
||||
},
|
||||
]}
|
||||
showPagination={false}
|
||||
noDataText={t('no_clients_found')}
|
||||
minRows={6}
|
||||
defaultPageSize={100}
|
||||
className="-highlight card-table-overflow--limited clients__table"
|
||||
getTrProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { ip } = rowInfo.original;
|
||||
const { ip } = rowInfo.original;
|
||||
|
||||
return getIpMatchListStatus(ip, disallowedClients)
|
||||
=== IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' };
|
||||
}}
|
||||
return getIpMatchListStatus(ip, disallowedClients) === IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' };
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Clients.propTypes = {
|
||||
topClients: PropTypes.array.isRequired,
|
||||
dnsQueries: PropTypes.number.isRequired,
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
clients: PropTypes.array.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
toggleClientStatus: PropTypes.func.isRequired,
|
||||
processingAccessSet: PropTypes.bool.isRequired,
|
||||
disallowedClients: PropTypes.string.isRequired,
|
||||
</Card>;
|
||||
};
|
||||
|
||||
export default withTranslation()(Clients);
|
||||
Clients.propTypes = {
|
||||
refreshButton: PropTypes.node.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
|
|
|
@ -47,32 +47,32 @@ const Counters = ({ refreshButton, subtitle }) => {
|
|||
label: 'dns_query',
|
||||
count: numDnsQueries,
|
||||
tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }),
|
||||
response_status: RESPONSE_FILTER.ALL.query,
|
||||
response_status: RESPONSE_FILTER.ALL.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'blocked_by',
|
||||
count: numBlockedFiltering,
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours',
|
||||
response_status: RESPONSE_FILTER.BLOCKED.query,
|
||||
response_status: RESPONSE_FILTER.BLOCKED.QUERY,
|
||||
translationComponents: [<a href="#filters" key="0">link</a>],
|
||||
},
|
||||
{
|
||||
label: 'stats_malware_phishing',
|
||||
count: numReplacedSafebrowsing,
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec',
|
||||
response_status: RESPONSE_FILTER.BLOCKED_THREATS.query,
|
||||
response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'stats_adult',
|
||||
count: numReplacedParental,
|
||||
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult',
|
||||
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.query,
|
||||
response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'enforced_save_search',
|
||||
count: numReplacedSafesearch,
|
||||
tooltipTitle: 'number_of_dns_query_to_safe_search',
|
||||
response_status: RESPONSE_FILTER.SAFE_SEARCH.query,
|
||||
response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY,
|
||||
},
|
||||
{
|
||||
label: 'average_processing_time',
|
||||
|
|
|
@ -10,7 +10,6 @@ import BlockedDomains from './BlockedDomains';
|
|||
|
||||
import PageTitle from '../ui/PageTitle';
|
||||
import Loading from '../ui/Loading';
|
||||
import { BLOCK_ACTIONS } from '../../helpers/constants';
|
||||
import './Dashboard.css';
|
||||
|
||||
const Dashboard = ({
|
||||
|
@ -19,7 +18,6 @@ const Dashboard = ({
|
|||
getStatsConfig,
|
||||
dashboard,
|
||||
toggleProtection,
|
||||
toggleClientBlock,
|
||||
stats,
|
||||
access,
|
||||
}) => {
|
||||
|
@ -50,14 +48,6 @@ const Dashboard = ({
|
|||
</button>;
|
||||
};
|
||||
|
||||
const toggleClientStatus = (type, ip) => {
|
||||
const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock';
|
||||
|
||||
if (window.confirm(t(confirmMessage, { ip }))) {
|
||||
toggleClientBlock(type, ip);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshButton = <button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm"
|
||||
|
@ -122,7 +112,6 @@ const Dashboard = ({
|
|||
clients={dashboard.clients}
|
||||
autoClients={dashboard.autoClients}
|
||||
refreshButton={refreshButton}
|
||||
toggleClientStatus={toggleClientStatus}
|
||||
processingAccessSet={access.processingSet}
|
||||
disallowedClients={access.disallowed_clients}
|
||||
/>
|
||||
|
@ -157,7 +146,6 @@ Dashboard.propTypes = {
|
|||
getStatsConfig: PropTypes.func.isRequired,
|
||||
toggleProtection: PropTypes.func.isRequired,
|
||||
getClients: PropTypes.func.isRequired,
|
||||
toggleClientBlock: PropTypes.func.isRequired,
|
||||
getAccessList: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -11,25 +11,10 @@ import {
|
|||
checkWhiteList,
|
||||
checkSafeSearch,
|
||||
checkSafeBrowsing,
|
||||
checkParental,
|
||||
checkParental, getFilterName,
|
||||
} from '../../../helpers/helpers';
|
||||
import { FILTERED } from '../../../helpers/constants';
|
||||
|
||||
const getFilterName = (id, filters, whitelistFilters, t) => {
|
||||
if (id === 0) {
|
||||
return t('filtered_custom_rules');
|
||||
}
|
||||
|
||||
const filter = filters.find((filter) => filter.id === id)
|
||||
|| whitelistFilters.find((filter) => filter.id === id);
|
||||
|
||||
if (filter && filter.name) {
|
||||
return t('query_log_filtered', { filter: filter.name });
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getTitle = (reason, filterName, t, onlyFiltered) => {
|
||||
if (checkNotFilteredNotFound(reason)) {
|
||||
return t('check_not_found');
|
||||
|
@ -101,7 +86,12 @@ const Info = ({
|
|||
ip_addrs,
|
||||
t,
|
||||
}) => {
|
||||
const filterName = getFilterName(filter_id, filters, whitelistFilters, t);
|
||||
const filterName = getFilterName(filters,
|
||||
whitelistFilters,
|
||||
filter_id,
|
||||
'filtered_custom_rules',
|
||||
(filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''));
|
||||
|
||||
const onlyFiltered = checkSafeSearch(reason)
|
||||
|| checkSafeBrowsing(reason)
|
||||
|| checkParental(reason);
|
||||
|
|
|
@ -48,7 +48,7 @@ class Table extends Component {
|
|||
accessor: 'url',
|
||||
minWidth: 200,
|
||||
Cell: ({ value }) => (
|
||||
<div className="logs__row o-hidden">
|
||||
<div className="logs__row">
|
||||
{isValidAbsolutePath(value) ? value
|
||||
: <a
|
||||
href={value}
|
||||
|
|
109
client/src/components/Logs/Cells/ClientCell.js
Normal file
109
client/src/components/Logs/Cells/ClientCell.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import React from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import propTypes from 'prop-types';
|
||||
import { checkFiltered } from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||
import { toggleBlocking } from '../../../actions';
|
||||
import IconTooltip from './IconTooltip';
|
||||
import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
|
||||
|
||||
const ClientCell = ({
|
||||
client,
|
||||
domain,
|
||||
info,
|
||||
info: { name, whois_info },
|
||||
reason,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||
const processingRules = useSelector((state) => state.filtering.processingRules);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const data = {
|
||||
address: client,
|
||||
name,
|
||||
country: whois_info?.country,
|
||||
city: whois_info?.city,
|
||||
network: whois_info?.orgname,
|
||||
source_label: source,
|
||||
};
|
||||
|
||||
const processedData = Object.entries(data);
|
||||
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||
'mt-2': isDetailed && !name && !whoisAvailable,
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const renderBlockingButton = (isFiltered, domain) => {
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const buttonClass = classNames('btn btn-sm logs__cell--block-button', {
|
||||
'btn-outline-secondary': isFiltered,
|
||||
'btn-outline-danger': !isFiltered,
|
||||
});
|
||||
|
||||
const onClick = () => dispatch(toggleBlocking(buttonType, domain));
|
||||
|
||||
return <button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>;
|
||||
};
|
||||
|
||||
return <div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell">
|
||||
<IconTooltip className={hintClass} columnClass='grid grid--limited' tooltipClass='px-5 pb-5 pt-4 mw-75'
|
||||
xlinkHref='question' contentItemClass="contentItemClass" title="client_details"
|
||||
content={processedData} placement="bottom" />
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{renderFormattedClientCell(client, info, isDetailed, true)}
|
||||
</div>
|
||||
{isDetailed && name && !whoisAvailable
|
||||
&& <div className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}>
|
||||
{name}
|
||||
</div>}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
ClientCell.propTypes = {
|
||||
client: propTypes.string.isRequired,
|
||||
domain: propTypes.string.isRequired,
|
||||
info: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.shape({
|
||||
name: propTypes.string.isRequired,
|
||||
whois_info: propTypes.shape({
|
||||
country: propTypes.string,
|
||||
city: propTypes.string,
|
||||
orgname: propTypes.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
reason: propTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ClientCell;
|
29
client/src/components/Logs/Cells/DateCell.js
Normal file
29
client/src/components/Logs/Cells/DateCell.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import propTypes from 'prop-types';
|
||||
import { formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||
import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT } from '../../../helpers/constants';
|
||||
|
||||
const DateCell = ({ time }) => {
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
if (!time) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
|
||||
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
|
||||
|
||||
return <div className="logs__cell logs__cell logs__cell--date text-truncate" role="gridcell">
|
||||
<div className="logs__time" title={formattedTime}>{formattedTime}</div>
|
||||
{isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={formattedDate}>{formattedDate}</div>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
DateCell.propTypes = {
|
||||
time: propTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DateCell;
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import getIconTooltip from './getIconTooltip';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
LONG_TIME_FORMAT,
|
||||
|
@ -9,15 +10,19 @@ import {
|
|||
} from '../../../helpers/constants';
|
||||
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
|
||||
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||
import IconTooltip from './IconTooltip';
|
||||
|
||||
const getDomainCell = (props) => {
|
||||
const {
|
||||
row, t, isDetailed, dnssec_enabled,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
tracker, type, answer_dnssec, client_proto, domain, time,
|
||||
} = row.original;
|
||||
const DomainCell = ({
|
||||
answer_dnssec,
|
||||
client_proto,
|
||||
domain,
|
||||
time,
|
||||
tracker,
|
||||
type,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
|
@ -50,8 +55,8 @@ const getDomainCell = (props) => {
|
|||
name_table_header: tracker?.name,
|
||||
category_label: hasTracker && captitalizeWords(tracker.category),
|
||||
source_label: sourceData
|
||||
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
|
||||
className="link--green">{sourceData.name}</a>,
|
||||
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
|
||||
className="link--green">{sourceData.name}</a>,
|
||||
};
|
||||
|
||||
const renderGrid = (content, idx) => {
|
||||
|
@ -72,51 +77,42 @@ const getDomainCell = (props) => {
|
|||
|
||||
const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
|
||||
|
||||
const trackerHint = getIconTooltip({
|
||||
className: privacyIconClass,
|
||||
tooltipClass: 'pt-4 pb-5 px-5 mw-75',
|
||||
xlinkHref: 'privacy',
|
||||
contentItemClass: 'key-colon',
|
||||
renderContent,
|
||||
place: 'bottom',
|
||||
});
|
||||
|
||||
const valueClass = classNames('w-100', {
|
||||
const valueClass = classNames('w-100 text-truncate', {
|
||||
'px-2 d-flex justify-content-center flex-column': isDetailed,
|
||||
});
|
||||
|
||||
const details = [ip, protocol].filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<div className="logs__row o-hidden">
|
||||
{dnssec_enabled && getIconTooltip({
|
||||
className: lockIconClass,
|
||||
tooltipClass: 'py-4 px-5 pb-45',
|
||||
canShowTooltip: answer_dnssec,
|
||||
xlinkHref: 'lock',
|
||||
columnClass: 'w-100',
|
||||
content: 'validated_with_dnssec',
|
||||
placement: 'bottom',
|
||||
})}
|
||||
{trackerHint}
|
||||
<div className={valueClass}>
|
||||
<div className="text-truncate" title={domain}>{domain}</div>
|
||||
{details && isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={details}>{details}</div>}
|
||||
</div>
|
||||
return <div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell">
|
||||
{dnssec_enabled && <IconTooltip
|
||||
className={lockIconClass}
|
||||
tooltipClass='py-4 px-5 pb-45'
|
||||
canShowTooltip={!!answer_dnssec}
|
||||
xlinkHref='lock'
|
||||
columnClass='w-100'
|
||||
content='validated_with_dnssec'
|
||||
placement='bottom'
|
||||
/>}
|
||||
<IconTooltip className={privacyIconClass} tooltipClass='pt-4 pb-5 px-5 mw-75'
|
||||
xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent}
|
||||
place='bottom' />
|
||||
<div className={valueClass}>
|
||||
<div className="text-truncate" title={domain}>{domain}</div>
|
||||
{details && isDetailed
|
||||
&& <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={details}>{details}</div>}
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
};
|
||||
|
||||
getDomainCell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
isDetailed: PropTypes.bool.isRequired,
|
||||
toggleBlocking: PropTypes.func.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
dnssec_enabled: PropTypes.bool.isRequired,
|
||||
DomainCell.propTypes = {
|
||||
answer_dnssec: propTypes.bool.isRequired,
|
||||
client_proto: propTypes.string.isRequired,
|
||||
domain: propTypes.string.isRequired,
|
||||
time: propTypes.string.isRequired,
|
||||
type: propTypes.string.isRequired,
|
||||
tracker: propTypes.object,
|
||||
};
|
||||
|
||||
export default getDomainCell;
|
||||
export default DomainCell;
|
54
client/src/components/Logs/Cells/Header.js
Normal file
54
client/src/components/Logs/Cells/Header.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { toggleDetailedLogs } from '../../../actions/queryLogs';
|
||||
import HeaderCell from './HeaderCell';
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
const disableDetailedMode = () => dispatch(toggleDetailedLogs(false));
|
||||
const enableDetailedMode = () => dispatch(toggleDetailedLogs(true));
|
||||
|
||||
const HEADERS = [
|
||||
{
|
||||
className: 'logs__cell--date',
|
||||
content: 'time_table_header',
|
||||
},
|
||||
{
|
||||
className: 'logs__cell--domain',
|
||||
content: 'request_table_header',
|
||||
},
|
||||
{
|
||||
className: 'logs__cell--response',
|
||||
content: 'response_table_header',
|
||||
},
|
||||
{
|
||||
className: 'logs__cell--client',
|
||||
content: <>
|
||||
{t('client_table_header')}
|
||||
{<span>
|
||||
<svg className={classNames('icons icon--24 icon--green cursor--pointer mr-2', { 'icon--selected': !isDetailed })}
|
||||
onClick={disableDetailedMode}
|
||||
>
|
||||
<title>{t('compact')}</title>
|
||||
<use xlinkHref='#list' /></svg>
|
||||
<svg className={classNames('icons icon--24 icon--green cursor--pointer', { 'icon--selected': isDetailed })}
|
||||
onClick={enableDetailedMode}
|
||||
>
|
||||
<title>{t('default')}</title>
|
||||
<use xlinkHref='#detailed_list' />
|
||||
</svg>
|
||||
</span>}
|
||||
</>,
|
||||
},
|
||||
];
|
||||
|
||||
return <div className="logs__cell--header__container px-5" role="row">
|
||||
{HEADERS.map(HeaderCell)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default Header;
|
22
client/src/components/Logs/Cells/HeaderCell.js
Normal file
22
client/src/components/Logs/Cells/HeaderCell.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const HeaderCell = ({ content, className }, idx) => {
|
||||
const { t } = useTranslation();
|
||||
return <div
|
||||
key={idx}
|
||||
className={classNames('logs__cell--header__item logs__cell logs__text--bold', className)}
|
||||
role="columnheader"
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>;
|
||||
};
|
||||
|
||||
HeaderCell.propTypes = {
|
||||
content: propTypes.oneOfType([propTypes.string, propTypes.element]).isRequired,
|
||||
className: propTypes.string,
|
||||
};
|
||||
|
||||
export default HeaderCell;
|
|
@ -7,7 +7,7 @@ import Tooltip from '../../ui/Tooltip';
|
|||
import 'react-popper-tooltip/dist/styles.css';
|
||||
import './IconTooltip.css';
|
||||
|
||||
const getIconTooltip = ({
|
||||
const IconTooltip = ({
|
||||
className,
|
||||
contentItemClass,
|
||||
columnClass,
|
||||
|
@ -43,14 +43,14 @@ const getIconTooltip = ({
|
|||
</Tooltip>;
|
||||
};
|
||||
|
||||
getIconTooltip.propTypes = {
|
||||
IconTooltip.propTypes = {
|
||||
className: PropTypes.string,
|
||||
contentItemClass: PropTypes.string,
|
||||
columnClass: PropTypes.string,
|
||||
tooltipClass: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
placement: PropTypes.string,
|
||||
canShowTooltip: PropTypes.string,
|
||||
canShowTooltip: PropTypes.bool,
|
||||
xlinkHref: PropTypes.string,
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
|
@ -59,4 +59,4 @@ getIconTooltip.propTypes = {
|
|||
renderContent: PropTypes.arrayOf(PropTypes.element),
|
||||
};
|
||||
|
||||
export default getIconTooltip;
|
||||
export default IconTooltip;
|
101
client/src/components/Logs/Cells/ResponseCell.js
Normal file
101
client/src/components/Logs/Cells/ResponseCell.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { formatElapsedMs, getFilterName } from '../../../helpers/helpers';
|
||||
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
|
||||
import IconTooltip from './IconTooltip';
|
||||
|
||||
const ResponseCell = ({
|
||||
elapsedMs,
|
||||
originalResponse,
|
||||
reason,
|
||||
response,
|
||||
status,
|
||||
upstream,
|
||||
rule,
|
||||
filterId,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
|
||||
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||
|
||||
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
||||
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
||||
|
||||
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
||||
|
||||
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
|
||||
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
|
||||
const filter = getFilterName(filters, whitelistFilters, filterId);
|
||||
|
||||
const renderResponses = (responseArr) => {
|
||||
if (!responseArr || responseArr.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return <div>{responseArr.map((response) => {
|
||||
const className = classNames('white-space--nowrap', {
|
||||
'overflow-break': response.length > 100,
|
||||
});
|
||||
|
||||
return <div key={response} className={className}>{`${response}\n`}</div>;
|
||||
})}</div>;
|
||||
};
|
||||
|
||||
const COMMON_CONTENT = {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_table_header: renderResponses(response),
|
||||
original_response: renderResponses(originalResponse),
|
||||
};
|
||||
|
||||
const content = rule
|
||||
? Object.entries(COMMON_CONTENT)
|
||||
: Object.entries({
|
||||
...COMMON_CONTENT,
|
||||
filter: '',
|
||||
});
|
||||
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
||||
|
||||
|
||||
return <div className="logs__cell logs__cell--response" role="gridcell">
|
||||
<IconTooltip
|
||||
className={classNames('icons mr-4 icon--24 icon--lightgray', { 'my-3': isDetailed })}
|
||||
columnClass='grid grid--limited'
|
||||
tooltipClass='px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details'
|
||||
contentItemClass='text-truncate key-colon o-hidden'
|
||||
xlinkHref='question'
|
||||
title='response_details'
|
||||
content={content}
|
||||
placement='bottom'
|
||||
/>
|
||||
<div className="text-truncate">
|
||||
<div className="text-truncate" title={statusLabel}>{statusLabel}</div>
|
||||
{isDetailed && <div
|
||||
className="detailed-info d-none d-sm-block pt-1 text-truncate"
|
||||
title={detailedInfo}>{detailedInfo}</div>}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
ResponseCell.propTypes = {
|
||||
elapsedMs: propTypes.string.isRequired,
|
||||
originalResponse: propTypes.array.isRequired,
|
||||
reason: propTypes.string.isRequired,
|
||||
response: propTypes.array.isRequired,
|
||||
status: propTypes.string.isRequired,
|
||||
upstream: propTypes.string.isRequired,
|
||||
rule: propTypes.string,
|
||||
filterId: propTypes.number,
|
||||
};
|
||||
|
||||
export default ResponseCell;
|
|
@ -1,110 +0,0 @@
|
|||
import React from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { formatClientCell } from '../../../helpers/formatClientCell';
|
||||
import getIconTooltip from './getIconTooltip';
|
||||
import { checkFiltered } from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS } from '../../../helpers/constants';
|
||||
|
||||
const getClientCell = ({
|
||||
row, t, isDetailed, toggleBlocking, autoClients, processingRules,
|
||||
}) => {
|
||||
const {
|
||||
reason, client, domain, info: { name, whois_info },
|
||||
} = row.original;
|
||||
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const data = {
|
||||
address: client,
|
||||
name,
|
||||
country: whois_info?.country,
|
||||
city: whois_info?.city,
|
||||
network: whois_info?.orgname,
|
||||
source_label: source,
|
||||
};
|
||||
|
||||
const processedData = Object.entries(data);
|
||||
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
|
||||
'mt-2': isDetailed && !name && !whoisAvailable,
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const renderBlockingButton = (isFiltered, domain) => {
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const buttonClass = classNames('logs__action button__action', {
|
||||
'btn-outline-secondary': isFiltered,
|
||||
'btn-outline-danger': !isFiltered,
|
||||
'logs__action--detailed': isDetailed,
|
||||
});
|
||||
|
||||
const onClick = () => toggleBlocking(buttonType, domain);
|
||||
|
||||
return (
|
||||
<div className={buttonClass}>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${buttonClass}`}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="logs__row o-hidden h-100">
|
||||
{getIconTooltip({
|
||||
className: hintClass,
|
||||
columnClass: 'grid grid--limited',
|
||||
tooltipClass: 'px-5 pb-5 pt-4 mw-75',
|
||||
xlinkHref: 'question',
|
||||
contentItemClass: 'text-truncate key-colon',
|
||||
title: 'client_details',
|
||||
content: processedData,
|
||||
placement: 'bottom',
|
||||
})}
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{formatClientCell(row, isDetailed)}
|
||||
</div>
|
||||
|
||||
{isDetailed && name && !whoisAvailable && (
|
||||
<div
|
||||
className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
getClientCell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
isDetailed: PropTypes.bool.isRequired,
|
||||
toggleBlocking: PropTypes.func.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
processingRules: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default getClientCell;
|
|
@ -1,28 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { formatTime, formatDateTime } from '../../../helpers/helpers';
|
||||
import {
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '../../../helpers/constants';
|
||||
|
||||
const getDateCell = (row, isDetailed) => {
|
||||
const { time } = row.original;
|
||||
|
||||
if (!time) {
|
||||
return '–';
|
||||
}
|
||||
|
||||
const formattedTime = formatTime(time, DEFAULT_TIME_FORMAT);
|
||||
const formattedDate = formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS);
|
||||
|
||||
return (
|
||||
<div className="logs__cell">
|
||||
<div className="logs__time" title={formattedTime}>{formattedTime}</div>
|
||||
{isDetailed && <div className="detailed-info d-none d-sm-block text-truncate"
|
||||
title={formattedDate}>{formattedDate}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default getDateCell;
|
|
@ -1,79 +0,0 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { formatElapsedMs } from '../../../helpers/helpers';
|
||||
import {
|
||||
FILTERED_STATUS,
|
||||
FILTERED_STATUS_TO_META_MAP,
|
||||
} from '../../../helpers/constants';
|
||||
import getIconTooltip from './getIconTooltip';
|
||||
|
||||
const getResponseCell = (row, filtering, t, isDetailed, getFilterName) => {
|
||||
const {
|
||||
reason, filterId, rule, status, upstream, elapsedMs, response, originalResponse,
|
||||
} = row.original;
|
||||
|
||||
const { filters, whitelistFilters } = filtering;
|
||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||
|
||||
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
||||
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
||||
|
||||
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
||||
|
||||
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.label || reason);
|
||||
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
|
||||
const filter = getFilterName(filters, whitelistFilters, filterId, t);
|
||||
|
||||
const renderResponses = (responseArr) => {
|
||||
if (responseArr?.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return <div>{responseArr.map((response) => {
|
||||
const className = classNames('white-space--nowrap', {
|
||||
'overflow-break': response.length > 100,
|
||||
});
|
||||
|
||||
return <div key={response} className={className}>{`${response}\n`}</div>;
|
||||
})}</div>;
|
||||
};
|
||||
|
||||
const COMMON_CONTENT = {
|
||||
encryption_status: boldStatusLabel,
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
filter,
|
||||
rule_label: rule,
|
||||
response_table_header: renderResponses(response),
|
||||
original_response: renderResponses(originalResponse),
|
||||
};
|
||||
|
||||
const content = rule
|
||||
? Object.entries(COMMON_CONTENT)
|
||||
: Object.entries({ ...COMMON_CONTENT, filter: '' });
|
||||
const detailedInfo = isBlocked ? filter : formattedElapsedMs;
|
||||
|
||||
return (
|
||||
<div className="logs__row">
|
||||
{getIconTooltip({
|
||||
className: classNames('icons mr-4 icon--24 icon--lightgray', { 'my-3': isDetailed }),
|
||||
columnClass: 'grid grid--limited',
|
||||
tooltipClass: 'px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details',
|
||||
contentItemClass: 'text-truncate key-colon o-hidden',
|
||||
xlinkHref: 'question',
|
||||
title: 'response_details',
|
||||
content,
|
||||
placement: 'bottom',
|
||||
})}
|
||||
<div className="text-truncate">
|
||||
<div className="text-truncate" title={statusLabel}>{statusLabel}</div>
|
||||
{isDetailed && <div
|
||||
className="detailed-info d-none d-sm-block pt-1 text-truncate"
|
||||
title={detailedInfo}>{detailedInfo}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default getResponseCell;
|
197
client/src/components/Logs/Cells/index.js
Normal file
197
client/src/components/Logs/Cells/index.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
import React, { memo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import propTypes from 'prop-types';
|
||||
import {
|
||||
captitalizeWords,
|
||||
checkFiltered,
|
||||
formatDateTime,
|
||||
formatElapsedMs,
|
||||
formatTime,
|
||||
getFilterName,
|
||||
processContent,
|
||||
} from '../../../helpers/helpers';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
FILTERED_STATUS,
|
||||
FILTERED_STATUS_TO_META_MAP,
|
||||
LONG_TIME_FORMAT,
|
||||
QUERY_STATUS_COLORS,
|
||||
SCHEME_TO_PROTOCOL_MAP,
|
||||
} from '../../../helpers/constants';
|
||||
import { getSourceData } from '../../../helpers/trackers/trackers';
|
||||
import { toggleBlocking } from '../../../actions';
|
||||
import DateCell from './DateCell';
|
||||
import DomainCell from './DomainCell';
|
||||
import ResponseCell from './ResponseCell';
|
||||
import ClientCell from './ClientCell';
|
||||
import '../Logs.css';
|
||||
|
||||
const Row = memo(({
|
||||
style,
|
||||
rowProps,
|
||||
rowProps: { reason },
|
||||
isSmallScreen,
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
|
||||
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
|
||||
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
|
||||
|
||||
const onClick = () => {
|
||||
if (!isSmallScreen) { return; }
|
||||
const {
|
||||
answer_dnssec,
|
||||
client,
|
||||
domain,
|
||||
elapsedMs,
|
||||
info,
|
||||
reason,
|
||||
response,
|
||||
time,
|
||||
tracker,
|
||||
upstream,
|
||||
type,
|
||||
client_proto,
|
||||
filterId,
|
||||
rule,
|
||||
originalResponse,
|
||||
status,
|
||||
} = rowProps;
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
const autoClient = autoClients
|
||||
.find((autoClient) => autoClient.name === client);
|
||||
|
||||
const { whois_info } = info;
|
||||
const country = whois_info?.country;
|
||||
const city = whois_info?.city;
|
||||
const network = whois_info?.orgname;
|
||||
|
||||
const source = autoClient?.source;
|
||||
|
||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
||||
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
||||
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
const onToggleBlock = () => {
|
||||
dispatch(toggleBlocking(buttonType, domain));
|
||||
};
|
||||
|
||||
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
||||
const requestStatus = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
|
||||
|
||||
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
|
||||
|
||||
const sourceData = getSourceData(tracker);
|
||||
|
||||
const filter = getFilterName(filters, whitelistFilters, filterId);
|
||||
|
||||
const detailedData = {
|
||||
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||
encryption_status: isBlocked
|
||||
? <div className="bg--danger">{requestStatus}</div> : requestStatus,
|
||||
domain,
|
||||
type_table_header: type,
|
||||
protocol,
|
||||
known_tracker: hasTracker && 'title',
|
||||
table_name: tracker?.name,
|
||||
category_label: hasTracker && captitalizeWords(tracker.category),
|
||||
tracker_source: hasTracker && sourceData
|
||||
&& <a
|
||||
href={sourceData.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link--green">{sourceData.name}
|
||||
</a>,
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter: rule ? filter : null,
|
||||
rule_label: rule,
|
||||
response_table_header: response?.join('\n'),
|
||||
response_code: status,
|
||||
client_details: 'title',
|
||||
ip_address: client,
|
||||
name: info?.name,
|
||||
country,
|
||||
city,
|
||||
network,
|
||||
source_label: source,
|
||||
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
||||
original_response: originalResponse?.join('\n'),
|
||||
[buttonType]: <div onClick={onToggleBlock}
|
||||
className={classNames('title--border text-center', {
|
||||
'bg--danger': isBlocked,
|
||||
})}>{t(buttonType)}</div>,
|
||||
};
|
||||
|
||||
setDetailedDataCurrent(processContent(detailedData));
|
||||
setButtonType(buttonType);
|
||||
setModalOpened(true);
|
||||
};
|
||||
|
||||
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
|
||||
|
||||
const className = classNames('d-flex px-5 logs__row',
|
||||
`logs__row--${FILTERED_STATUS_TO_META_MAP?.[reason]?.COLOR ?? QUERY_STATUS_COLORS.WHITE}`, {
|
||||
'logs__cell--detailed': isDetailed,
|
||||
});
|
||||
|
||||
return <div style={style} className={className} onClick={onClick} role="row">
|
||||
<DateCell {...rowProps} />
|
||||
<DomainCell {...rowProps} />
|
||||
<ResponseCell {...rowProps} />
|
||||
<ClientCell {...rowProps} />
|
||||
</div>;
|
||||
});
|
||||
|
||||
Row.displayName = 'Row';
|
||||
|
||||
Row.propTypes = {
|
||||
style: propTypes.object,
|
||||
rowProps: propTypes.shape({
|
||||
reason: propTypes.string.isRequired,
|
||||
answer_dnssec: propTypes.bool.isRequired,
|
||||
client: propTypes.string.isRequired,
|
||||
domain: propTypes.string.isRequired,
|
||||
elapsedMs: propTypes.string.isRequired,
|
||||
info: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.shape({
|
||||
whois_info: propTypes.shape({
|
||||
country: propTypes.string,
|
||||
city: propTypes.string,
|
||||
orgname: propTypes.string,
|
||||
}),
|
||||
})]),
|
||||
response: propTypes.array.isRequired,
|
||||
time: propTypes.string.isRequired,
|
||||
tracker: propTypes.object,
|
||||
upstream: propTypes.string.isRequired,
|
||||
type: propTypes.string.isRequired,
|
||||
client_proto: propTypes.string.isRequired,
|
||||
filterId: propTypes.number,
|
||||
rule: propTypes.string,
|
||||
originalResponse: propTypes.array,
|
||||
status: propTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
isSmallScreen: propTypes.bool.isRequired,
|
||||
setDetailedDataCurrent: propTypes.func.isRequired,
|
||||
setButtonType: propTypes.func.isRequired,
|
||||
setModalOpened: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Row;
|
|
@ -107,7 +107,7 @@ const Form = (props) => {
|
|||
|
||||
const {
|
||||
response_status, search,
|
||||
} = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
} = useSelector((state) => state?.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
|
||||
|
||||
const [
|
||||
debouncedSearch,
|
||||
|
@ -171,14 +171,14 @@ const Form = (props) => {
|
|||
>
|
||||
{Object.values(RESPONSE_FILTER)
|
||||
.map(({
|
||||
query, label, disabled,
|
||||
QUERY, LABEL, disabled,
|
||||
}) => (
|
||||
<option
|
||||
key={label}
|
||||
value={query}
|
||||
key={LABEL}
|
||||
value={QUERY}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t(label)}
|
||||
{t(LABEL)}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
|
@ -197,5 +197,4 @@ Form.propTypes = {
|
|||
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Form from './Form';
|
||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../../actions/toasts';
|
||||
|
||||
const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
||||
const Filters = ({ filter, setIsLoading }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const refreshLogs = async () => {
|
||||
setIsLoading(true);
|
||||
await dispatch(refreshFilteredLogs());
|
||||
dispatch(addSuccessToast('query_log_updated'));
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return <div className="page-header page-header--logs">
|
||||
<h1 className="page-title page-title--large">
|
||||
|
@ -29,7 +40,6 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
|
|||
|
||||
Filters.propTypes = {
|
||||
filter: PropTypes.object.isRequired,
|
||||
refreshLogs: PropTypes.func.isRequired,
|
||||
processingGetLogs: PropTypes.bool.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
87
client/src/components/Logs/InfiniteTable.js
Normal file
87
client/src/components/Logs/InfiniteTable.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import propTypes from 'prop-types';
|
||||
import throttle from 'lodash/throttle';
|
||||
import Loading from '../ui/Loading';
|
||||
import Header from './Cells/Header';
|
||||
import { getLogs } from '../../actions/queryLogs';
|
||||
import Row from './Cells';
|
||||
import { isScrolledIntoView } from '../../helpers/helpers';
|
||||
import { QUERY_LOGS_PAGE_LIMIT } from '../../helpers/constants';
|
||||
|
||||
const InfiniteTable = ({
|
||||
isLoading,
|
||||
items,
|
||||
isSmallScreen,
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const loader = useRef(null);
|
||||
|
||||
const {
|
||||
isEntireLog,
|
||||
processingGetLogs,
|
||||
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
|
||||
const loading = isLoading || processingGetLogs;
|
||||
|
||||
const listener = useCallback(() => {
|
||||
if (loader.current && isScrolledIntoView(loader.current)) {
|
||||
dispatch(getLogs());
|
||||
}
|
||||
}, [loader.current, isScrolledIntoView, getLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
listener();
|
||||
}, [items.length < QUERY_LOGS_PAGE_LIMIT]);
|
||||
|
||||
useEffect(() => {
|
||||
const THROTTLE_TIME = 100;
|
||||
const throttledListener = throttle(listener, THROTTLE_TIME);
|
||||
|
||||
window.addEventListener('scroll', throttledListener);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', throttledListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderRow = (row, idx) => <Row
|
||||
key={idx}
|
||||
rowProps={row}
|
||||
isSmallScreen={isSmallScreen}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
/>;
|
||||
|
||||
const isNothingFound = items.length === 0 && !processingGetLogs;
|
||||
|
||||
return <div className='logs__table' role='grid'>
|
||||
{loading && <Loading />}
|
||||
<Header />
|
||||
{isNothingFound
|
||||
? <label className="logs__no-data">{t('nothing_found')}</label>
|
||||
: <>{items.map(renderRow)}
|
||||
{!isEntireLog && <div ref={loader} className="logs__loading text-center">{t('loading_table_status')}</div>}
|
||||
</>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
InfiniteTable.propTypes = {
|
||||
isLoading: propTypes.bool.isRequired,
|
||||
items: propTypes.array.isRequired,
|
||||
isSmallScreen: propTypes.bool.isRequired,
|
||||
setDetailedDataCurrent: propTypes.func.isRequired,
|
||||
setButtonType: propTypes.func.isRequired,
|
||||
setModalOpened: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default InfiniteTable;
|
|
@ -1,44 +1,21 @@
|
|||
:root {
|
||||
--blue: #e5effd;
|
||||
--green-pale: rgba(103, 178, 121, 0.1);
|
||||
--red: rgba(223, 56, 18, 0.05);
|
||||
--white: #fff;
|
||||
--yellow: rgba(247, 181, 0, 0.1);
|
||||
--size-date: 70;
|
||||
--size-domain: 180;
|
||||
--size-response: 150;
|
||||
--size-client: 123;
|
||||
--gray-216: rgba(216, 216, 216, 0.23);
|
||||
--gray-4d: #4D4D4D;
|
||||
--gray-8: #888;
|
||||
--danger: #DF3812;
|
||||
--white80: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.logs__row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 26px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.card-table .logs__row {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logs__row--center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs__row--column {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs__row--icons {
|
||||
max-width: 180px;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.logs__row .list-unstyled {
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__text,
|
||||
.logs__row .list-unstyled li {
|
||||
.logs__text {
|
||||
padding: 0 1px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -54,237 +31,6 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logs__text--full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logs__text--wrap {
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.logs__text--nowrap {
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logs__text--whois {
|
||||
line-height: 1.2;
|
||||
color: #9aa0ac;
|
||||
}
|
||||
|
||||
.logs__row .tooltip-custom {
|
||||
top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tooltip__option {
|
||||
height: 2.5rem !important;
|
||||
width: 10.5rem;
|
||||
padding: 0.3125rem 1.5rem 0.6875rem;
|
||||
}
|
||||
|
||||
.tooltip__option:hover {
|
||||
background-color: var(--gray-f3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button__action {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.table__action {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.logs__action {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.logs__action--detailed {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.logs__table .rt-td,
|
||||
.clients__table .rt-td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead, .logs__table .rt-tbody {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr:hover .logs__action,
|
||||
.clients__table .rt-tr:hover .table__action {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .tooltip-custom:before {
|
||||
top: calc(100% + 12px);
|
||||
bottom: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .tooltip-custom:after {
|
||||
top: initial;
|
||||
bottom: -4px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .popover__body {
|
||||
top: calc(100% + 5px);
|
||||
bottom: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:first-child .popover__body:after {
|
||||
top: -11px;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid #585965;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters input,
|
||||
.logs__table .rt-thead.-filters select {
|
||||
padding: 6px 7px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: #495057;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters select {
|
||||
background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center;
|
||||
background-size: 8px 10px;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-filters input:focus,
|
||||
.logs__table .rt-thead.-filters select:focus {
|
||||
border-color: #1991eb;
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
|
||||
}
|
||||
|
||||
.logs__text-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.logs__list-wrap {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.logs__list-item {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs__input-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logs__whois {
|
||||
display: inline;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logs__whois::after {
|
||||
content: "|";
|
||||
padding: 0 5px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.logs__whois:last-child::after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.logs__whois-icon.icons {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 1px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* New logs */
|
||||
.logs__table {
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
min-height: 42rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.logs__table--detailed {
|
||||
min-height: 50rem;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead.-header {
|
||||
box-shadow: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead .rt-th {
|
||||
padding: 0.9375rem 0.9375rem 0.875rem 0;
|
||||
text-align: left;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-tbody .rt-td {
|
||||
padding: 1rem 1rem 0.5rem 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-thead .rt-th:last-child,
|
||||
.logs__table .rt-tbody .rt-td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-tbody .rt-tr-group {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr {
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr {
|
||||
position: relative;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:not(:first-child) .rt-tr:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
right: 1.5rem;
|
||||
top: 0;
|
||||
width: calc(100% - 3rem);
|
||||
height: 2px;
|
||||
background-color: rgba(216, 216, 216, 0.23);
|
||||
}
|
||||
|
||||
.logs__table .rt-tr-group:last-child .rt-tr:after,
|
||||
.logs__table .rt-thead .rt-tr:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logs__time {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
|
@ -302,132 +48,24 @@
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Hide 3 and 4 column on mobile */
|
||||
.logs__table .rt-thead .rt-th:nth-child(3),
|
||||
.logs__table .rt-thead .rt-th:nth-child(4),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(3),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.logs__table .rt-thead .rt-th:nth-child(3),
|
||||
.logs__table .rt-thead .rt-th:nth-child(4),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(3),
|
||||
.logs__table .rt-tbody .rt-td:nth-child(4) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.text-pre {
|
||||
white-space: pre-wrap !important;
|
||||
overflow-wrap: break-word;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
width: 11.875rem !important;
|
||||
background-color: transparent;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.custom-pagination--padding {
|
||||
padding: 2.5rem 0 2.5rem !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-btn {
|
||||
--side-size: 2rem;
|
||||
background-color: transparent !important;
|
||||
border: 1px solid var(--gray-d8) !important;
|
||||
border-radius: 4px !important;
|
||||
width: var(--side-size) !important;
|
||||
height: var(--side-size) !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-btn:enabled:hover {
|
||||
background-color: var(--gray-f3) !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-previous {
|
||||
flex: 0 1 !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-next {
|
||||
flex: 0 1 !important;
|
||||
}
|
||||
|
||||
.custom-pagination .-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.logs__table .-pageInfo {
|
||||
--side-size: 2rem;
|
||||
font-variant-numeric: tabular-nums !important;
|
||||
background-color: transparent !important;
|
||||
border: 1px solid var(--gray-d8) !important;
|
||||
border-radius: 4px !important;
|
||||
width: var(--side-size) !important;
|
||||
height: var(--side-size) !important;
|
||||
margin: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logs__table .pagination-bottom {
|
||||
justify-content: center !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.logs__table .-center:before {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.logs__table .-center:after {
|
||||
content: '...';
|
||||
transform: translateY(-0.25rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.icon--detailed-info {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.link--green {
|
||||
color: var(--green79);
|
||||
}
|
||||
|
||||
.row--detailed {
|
||||
height: 4.9rem
|
||||
}
|
||||
|
||||
.w-90 {
|
||||
max-width: 90% !important;
|
||||
}
|
||||
|
||||
.h-85 {
|
||||
height: 85% !important;
|
||||
}
|
||||
|
||||
.pt-45 {
|
||||
padding-top: 1.25rem !important;
|
||||
}
|
||||
|
||||
.pb-45 {
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
|
||||
.py-45 {
|
||||
padding-top: 1.25rem !important;
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
|
||||
.mh-100 {
|
||||
max-height: 100% !important;
|
||||
}
|
||||
|
@ -493,14 +131,6 @@
|
|||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.rt-tr .logs__row .logs__text {
|
||||
max-width: calc(100% - 1.5rem);
|
||||
}
|
||||
|
||||
.ml-small {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control--container {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
@ -517,38 +147,157 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.logs__table .rt-tr {
|
||||
height: 3.125rem;
|
||||
@media screen and (max-width: 767.98px) {
|
||||
.logs__table .logs__cell--response,
|
||||
.logs__table .logs__cell--client {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.logs__table .rt-tbody .rt-td {
|
||||
padding: 0.625rem 1rem 0.875rem 0;
|
||||
}
|
||||
|
||||
.logs__table {
|
||||
min-height: 42rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loading__container > .-loading-inner {
|
||||
top: 10rem !important;
|
||||
bottom: initial !important;
|
||||
}
|
||||
|
||||
.loading__text {
|
||||
transform: translateY(3rem);
|
||||
}
|
||||
|
||||
.logs__refresh {
|
||||
--size: 2.5rem;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
margin-left: 0.9375rem;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.logs__cell {
|
||||
padding: 1rem 1rem 0.5rem 0;
|
||||
}
|
||||
|
||||
.logs__cell--date {
|
||||
width: 4.375rem;
|
||||
flex: var(--size-date) 0 auto;
|
||||
}
|
||||
|
||||
.logs__cell--domain {
|
||||
width: 11.25rem;
|
||||
flex: var(--size-domain) 0 auto;
|
||||
}
|
||||
|
||||
.logs__cell--response {
|
||||
width: 9.375rem;
|
||||
flex: var(--size-response) 0 auto;
|
||||
}
|
||||
|
||||
.logs__cell--client {
|
||||
width: 7.6875rem;
|
||||
flex: var(--size-client) 0 auto;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.logs__cell--header__container > .logs__cell--header__item {
|
||||
border-right: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.logs__cell--header__container > .logs__cell--header__item:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.logs__cell--block-button {
|
||||
max-height: 1.75rem;
|
||||
position: relative;
|
||||
left: 10%;
|
||||
top: 40%;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.logs__row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 26px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logs__table .logs__row {
|
||||
border-bottom: 2px solid var(--gray-216);
|
||||
}
|
||||
|
||||
.logs__table .logs__row:hover .logs__cell--block-button {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.logs__table .logs__row .logs__cell--block-button:disabled {
|
||||
background-color: var(--white) !important;
|
||||
}
|
||||
|
||||
/* QUERY_STATUS_COLORS */
|
||||
.logs__row--blue {
|
||||
background-color: var(--blue);
|
||||
}
|
||||
|
||||
.logs__row--green {
|
||||
background-color: var(--green-pale);
|
||||
}
|
||||
|
||||
.logs__row--red {
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
.logs__row--white {
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.logs__row--yellow {
|
||||
background-color: var(--yellow);
|
||||
}
|
||||
|
||||
.logs__no-data {
|
||||
color: var(--gray-4d);
|
||||
background-color: var(--white80);
|
||||
pointer-events: none;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding-top: 21rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logs__loading {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.logs__table {
|
||||
background-color: var(--white);
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
min-height: 43rem;
|
||||
max-width: 100%;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
contain: layout;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.logs__table .logs__cell--response,
|
||||
.logs__table .logs__cell--client {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logs__cell--header__container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logs__table > .logs__cell--header__container > .logs__cell--client {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logs__table .loading:after {
|
||||
top: 10%;
|
||||
}
|
||||
|
||||
.logs__table .loading:before {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
|
|
@ -1,414 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import ReactTable from 'react-table';
|
||||
import classNames from 'classnames';
|
||||
import endsWith from 'lodash/endsWith';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
|
||||
LONG_TIME_FORMAT,
|
||||
FILTERED_STATUS_TO_META_MAP,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
SCHEME_TO_PROTOCOL_MAP,
|
||||
CUSTOM_FILTERING_RULES_ID, FILTERED_STATUS,
|
||||
} from '../../helpers/constants';
|
||||
import getDateCell from './Cells/getDateCell';
|
||||
import getDomainCell from './Cells/getDomainCell';
|
||||
import getClientCell from './Cells/getClientCell';
|
||||
import getResponseCell from './Cells/getResponseCell';
|
||||
|
||||
import {
|
||||
captitalizeWords,
|
||||
checkFiltered,
|
||||
formatDateTime,
|
||||
formatElapsedMs,
|
||||
formatTime,
|
||||
processContent,
|
||||
} from '../../helpers/helpers';
|
||||
import Loading from '../ui/Loading';
|
||||
import { getSourceData } from '../../helpers/trackers/trackers';
|
||||
|
||||
const Table = (props) => {
|
||||
const {
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
isSmallScreen,
|
||||
setIsLoading,
|
||||
filtering,
|
||||
isDetailed,
|
||||
toggleDetailedLogs,
|
||||
setLogsPage,
|
||||
setLogsPagination,
|
||||
processingGetLogs,
|
||||
logs,
|
||||
pages,
|
||||
page,
|
||||
isLoading,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleBlocking = (type, domain) => {
|
||||
const {
|
||||
setRules, getFilteringStatus, addSuccessToast,
|
||||
} = props;
|
||||
const { userRules } = filtering;
|
||||
|
||||
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
||||
const baseRule = `||${domain}^$important`;
|
||||
const baseUnblocking = `@@${baseRule}`;
|
||||
|
||||
const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule;
|
||||
const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking;
|
||||
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
||||
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
||||
|
||||
const matchPreparedBlockingRule = userRules.match(preparedBlockingRule);
|
||||
const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule);
|
||||
|
||||
if (matchPreparedBlockingRule) {
|
||||
setRules(userRules.replace(`${blockingRule}`, ''));
|
||||
addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
||||
} else if (!matchPreparedUnblockingRule) {
|
||||
setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
|
||||
addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
||||
} else if (matchPreparedUnblockingRule) {
|
||||
addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
||||
return;
|
||||
} else if (!matchPreparedBlockingRule) {
|
||||
addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
||||
return;
|
||||
}
|
||||
|
||||
getFilteringStatus();
|
||||
};
|
||||
|
||||
const getFilterName = (filters, whitelistFilters, filterId, t) => {
|
||||
if (filterId === CUSTOM_FILTERING_RULES_ID) {
|
||||
return t('custom_filter_rules');
|
||||
}
|
||||
|
||||
const filter = filters.find((filter) => filter.id === filterId)
|
||||
|| whitelistFilters.find((filter) => filter.id === filterId);
|
||||
let filterName = '';
|
||||
|
||||
if (filter) {
|
||||
filterName = filter.name;
|
||||
}
|
||||
|
||||
if (!filterName) {
|
||||
filterName = t('unknown_filter', { filterId });
|
||||
}
|
||||
|
||||
return filterName;
|
||||
};
|
||||
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: t('time_table_header'),
|
||||
accessor: 'time',
|
||||
Cell: (row) => getDateCell(row, isDetailed),
|
||||
minWidth: 70,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
},
|
||||
{
|
||||
Header: t('request_table_header'),
|
||||
accessor: 'domain',
|
||||
Cell: (row) => {
|
||||
const {
|
||||
isDetailed,
|
||||
autoClients,
|
||||
dnssec_enabled,
|
||||
} = props;
|
||||
|
||||
return getDomainCell({
|
||||
row,
|
||||
t,
|
||||
isDetailed,
|
||||
toggleBlocking,
|
||||
autoClients,
|
||||
dnssec_enabled,
|
||||
});
|
||||
},
|
||||
minWidth: 180,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
},
|
||||
{
|
||||
Header: t('response_table_header'),
|
||||
accessor: 'response',
|
||||
Cell: (row) => getResponseCell(
|
||||
row,
|
||||
filtering,
|
||||
t,
|
||||
isDetailed,
|
||||
getFilterName,
|
||||
),
|
||||
minWidth: 150,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
},
|
||||
{
|
||||
Header: function Header() {
|
||||
return <div className="d-flex justify-content-between">
|
||||
{t('client_table_header')}
|
||||
{<span>
|
||||
<svg
|
||||
className={classNames('icons icon--24 icon--green mr-2 cursor--pointer', {
|
||||
'icon--selected': !isDetailed,
|
||||
})}
|
||||
onClick={() => toggleDetailedLogs(false)}
|
||||
>
|
||||
<title><Trans>compact</Trans></title>
|
||||
<use xlinkHref='#list' />
|
||||
</svg>
|
||||
<svg
|
||||
className={classNames('icons icon--24 icon--green cursor--pointer', {
|
||||
'icon--selected': isDetailed,
|
||||
})}
|
||||
onClick={() => toggleDetailedLogs(true)}
|
||||
>
|
||||
<title><Trans>default</Trans></title>
|
||||
<use xlinkHref='#detailed_list' />
|
||||
</svg>
|
||||
</span>}
|
||||
</div>;
|
||||
},
|
||||
accessor: 'client',
|
||||
Cell: (row) => {
|
||||
const {
|
||||
isDetailed,
|
||||
autoClients,
|
||||
filtering: { processingRules },
|
||||
} = props;
|
||||
|
||||
return getClientCell({
|
||||
row,
|
||||
t,
|
||||
isDetailed,
|
||||
toggleBlocking,
|
||||
autoClients,
|
||||
processingRules,
|
||||
});
|
||||
},
|
||||
minWidth: 123,
|
||||
maxHeight: 60,
|
||||
headerClassName: 'logs__text',
|
||||
className: 'pb-0',
|
||||
},
|
||||
];
|
||||
|
||||
const changePage = async (page) => {
|
||||
setIsLoading(true);
|
||||
|
||||
const { oldest, getLogs, pages } = props;
|
||||
const isLastPage = pages && (page + 1 === pages);
|
||||
|
||||
await Promise.all([
|
||||
setLogsPage(page),
|
||||
setLogsPagination({
|
||||
page,
|
||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||
}),
|
||||
].concat(isLastPage ? getLogs(oldest, page) : []));
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const tableClass = classNames('logs__table', {
|
||||
'logs__table--detailed': isDetailed,
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactTable
|
||||
manual
|
||||
minRows={0}
|
||||
page={page}
|
||||
pages={pages}
|
||||
columns={columns}
|
||||
filterable={false}
|
||||
sortable={false}
|
||||
resizable={false}
|
||||
data={logs || []}
|
||||
loading={isLoading || processingGetLogs}
|
||||
showPageJump={false}
|
||||
showPageSizeOptions={false}
|
||||
onPageChange={changePage}
|
||||
className={tableClass}
|
||||
defaultPageSize={TABLE_DEFAULT_PAGE_SIZE}
|
||||
loadingText={
|
||||
<>
|
||||
<Loading />
|
||||
<h6 className="loading__text">{t('loading_table_status')}</h6>
|
||||
</>
|
||||
}
|
||||
getLoadingProps={() => ({ className: 'loading__container' })}
|
||||
rowsText={t('rows_table_footer_text')}
|
||||
noDataText={!processingGetLogs
|
||||
&& <label className="logs__text logs__text--bold">{t('nothing_found')}</label>}
|
||||
pageText=''
|
||||
ofText=''
|
||||
showPagination={logs.length > 0}
|
||||
getPaginationProps={() => ({ className: 'custom-pagination custom-pagination--padding' })}
|
||||
getTbodyProps={() => ({ className: 'd-block' })}
|
||||
previousText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100 cursor--pointer">
|
||||
<title><Trans>previous_btn</Trans></title>
|
||||
<use xlinkHref="#arrow-left" />
|
||||
</svg>}
|
||||
nextText={
|
||||
<svg className="icons icon--24 icon--gray w-100 h-100 cursor--pointer">
|
||||
<title><Trans>next_btn</Trans></title>
|
||||
<use xlinkHref="#arrow-right" />
|
||||
</svg>}
|
||||
renderTotalPagesCount={() => false}
|
||||
getTrGroupProps={(_state, rowInfo) => {
|
||||
if (!rowInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { reason } = rowInfo.original;
|
||||
const colorClass = FILTERED_STATUS_TO_META_MAP[reason] ? FILTERED_STATUS_TO_META_MAP[reason].color : 'white';
|
||||
|
||||
return { className: colorClass };
|
||||
}}
|
||||
getTrProps={(state, rowInfo) => ({
|
||||
className: isDetailed ? 'row--detailed' : '',
|
||||
onClick: () => {
|
||||
if (isSmallScreen) {
|
||||
const { dnssec_enabled, autoClients } = props;
|
||||
const {
|
||||
answer_dnssec,
|
||||
client,
|
||||
domain,
|
||||
elapsedMs,
|
||||
info,
|
||||
reason,
|
||||
response,
|
||||
time,
|
||||
tracker,
|
||||
upstream,
|
||||
type,
|
||||
client_proto,
|
||||
filterId,
|
||||
rule,
|
||||
originalResponse,
|
||||
status,
|
||||
} = rowInfo.original;
|
||||
|
||||
const hasTracker = !!tracker;
|
||||
|
||||
const autoClient = autoClients
|
||||
.find((autoClient) => autoClient.name === client);
|
||||
|
||||
const { whois_info } = info;
|
||||
const country = whois_info?.country;
|
||||
const city = whois_info?.city;
|
||||
const network = whois_info?.orgname;
|
||||
|
||||
const source = autoClient?.source;
|
||||
|
||||
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
|
||||
const isFiltered = checkFiltered(reason);
|
||||
|
||||
const isBlocked = reason === FILTERED_STATUS.FILTERED_BLACK_LIST
|
||||
|| reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
||||
|
||||
const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
const onToggleBlock = () => {
|
||||
toggleBlocking(buttonType, domain);
|
||||
};
|
||||
|
||||
const isBlockedByResponse = originalResponse.length > 0 && isBlocked;
|
||||
const requestStatus = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.label || reason);
|
||||
|
||||
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
|
||||
|
||||
const sourceData = getSourceData(tracker);
|
||||
|
||||
const { filters, whitelistFilters } = filtering;
|
||||
const filter = getFilterName(filters, whitelistFilters, filterId, t);
|
||||
|
||||
const detailedData = {
|
||||
time_table_header: formatTime(time, LONG_TIME_FORMAT),
|
||||
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
|
||||
encryption_status: isBlocked
|
||||
? <div className="bg--danger">{requestStatus}</div> : requestStatus,
|
||||
domain,
|
||||
type_table_header: type,
|
||||
protocol,
|
||||
known_tracker: hasTracker && 'title',
|
||||
table_name: tracker?.name,
|
||||
category_label: hasTracker && captitalizeWords(tracker.category),
|
||||
tracker_source: hasTracker && sourceData
|
||||
&& <a
|
||||
href={sourceData.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link--green">{sourceData.name}
|
||||
</a>,
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter: rule ? filter : null,
|
||||
rule_label: rule,
|
||||
response_table_header: response?.join('\n'),
|
||||
response_code: status,
|
||||
client_details: 'title',
|
||||
ip_address: client,
|
||||
name: info?.name,
|
||||
country,
|
||||
city,
|
||||
network,
|
||||
source_label: source,
|
||||
validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
|
||||
original_response: originalResponse?.join('\n'),
|
||||
[buttonType]: <div onClick={onToggleBlock}
|
||||
className={classNames('title--border text-center', {
|
||||
'bg--danger': isBlocked,
|
||||
})}>{t(buttonType)}</div>,
|
||||
};
|
||||
|
||||
setDetailedDataCurrent(processContent(detailedData));
|
||||
setButtonType(buttonType);
|
||||
setModalOpened(true);
|
||||
}
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Table.propTypes = {
|
||||
logs: PropTypes.array.isRequired,
|
||||
pages: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
autoClients: PropTypes.array.isRequired,
|
||||
defaultPageSize: PropTypes.number,
|
||||
oldest: PropTypes.string.isRequired,
|
||||
filtering: PropTypes.object.isRequired,
|
||||
processingGetLogs: PropTypes.bool.isRequired,
|
||||
processingGetConfig: PropTypes.bool.isRequired,
|
||||
isDetailed: PropTypes.bool.isRequired,
|
||||
setLogsPage: PropTypes.func.isRequired,
|
||||
setLogsPagination: PropTypes.func.isRequired,
|
||||
getLogs: PropTypes.func.isRequired,
|
||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
||||
setRules: PropTypes.func.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
getFilteringStatus: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
setIsLoading: PropTypes.func.isRequired,
|
||||
dnssec_enabled: PropTypes.bool.isRequired,
|
||||
setDetailedDataCurrent: PropTypes.func.isRequired,
|
||||
setButtonType: PropTypes.func.isRequired,
|
||||
setModalOpened: PropTypes.func.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Table;
|
|
@ -1,5 +1,4 @@
|
|||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
@ -8,24 +7,21 @@ import queryString from 'query-string';
|
|||
import classNames from 'classnames';
|
||||
import {
|
||||
BLOCK_ACTIONS,
|
||||
TABLE_DEFAULT_PAGE_SIZE,
|
||||
TABLE_FIRST_PAGE,
|
||||
SMALL_SCREEN_SIZE,
|
||||
} from '../../helpers/constants';
|
||||
import Loading from '../ui/Loading';
|
||||
import Filters from './Filters';
|
||||
import Table from './Table';
|
||||
import Disabled from './Disabled';
|
||||
import { getFilteringStatus } from '../../actions/filtering';
|
||||
import { getClients } from '../../actions';
|
||||
import { getDnsConfig } from '../../actions/dnsConfig';
|
||||
import {
|
||||
getLogsConfig,
|
||||
refreshFilteredLogs,
|
||||
resetFilteredLogs,
|
||||
setFilteredLogs,
|
||||
toggleDetailedLogs,
|
||||
} from '../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../actions/toasts';
|
||||
import InfiniteTable from './InfiniteTable';
|
||||
import './Logs.css';
|
||||
|
||||
const processContent = (data, buttonType) => Object.entries(data)
|
||||
|
@ -48,21 +44,20 @@ const processContent = (data, buttonType) => Object.entries(data)
|
|||
keyClass = '';
|
||||
}
|
||||
|
||||
return isHidden ? null : <Fragment key={key}>
|
||||
return isHidden ? null : <div key={key}>
|
||||
<div
|
||||
className={classNames(`key__${key}`, keyClass, {
|
||||
'font-weight-bold': isBoolean && value === true,
|
||||
})}>
|
||||
className={classNames(`key__${key}`, keyClass, {
|
||||
'font-weight-bold': isBoolean && value === true,
|
||||
})}>
|
||||
<Trans>{isButton ? value : key}</Trans>
|
||||
</div>
|
||||
<div className={`value__${key} text-pre text-truncate`}>
|
||||
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
||||
</div>
|
||||
</Fragment>;
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
||||
const Logs = (props) => {
|
||||
const Logs = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -71,7 +66,14 @@ const Logs = (props) => {
|
|||
search: search_url_param = '',
|
||||
} = queryString.parse(history.location.search);
|
||||
|
||||
const { filter } = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
const {
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
const filter = useSelector((state) => state.queryLogs.filter, shallowEqual);
|
||||
const logs = useSelector((state) => state.queryLogs.logs, shallowEqual);
|
||||
|
||||
const search = filter?.search || search_url_param;
|
||||
const response_status = filter?.response_status || response_status_url_param;
|
||||
|
@ -82,6 +84,7 @@ const Logs = (props) => {
|
|||
const [isModalOpened, setModalOpened] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const closeModal = () => setModalOpened(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
@ -94,44 +97,11 @@ const Logs = (props) => {
|
|||
})();
|
||||
}, [response_status, search]);
|
||||
|
||||
const {
|
||||
filtering,
|
||||
setLogsPage,
|
||||
setLogsPagination,
|
||||
toggleDetailedLogs,
|
||||
dashboard,
|
||||
dnsConfig,
|
||||
queryLogs: {
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
oldest,
|
||||
logs,
|
||||
pages,
|
||||
page,
|
||||
isDetailed,
|
||||
},
|
||||
} = props;
|
||||
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`);
|
||||
const mediaQueryHandler = (e) => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
toggleDetailedLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => setModalOpened(false);
|
||||
|
||||
const getLogs = (older_than, page, initial) => {
|
||||
if (enabled) {
|
||||
props.getLogs({
|
||||
older_than,
|
||||
page,
|
||||
pageSize: TABLE_DEFAULT_PAGE_SIZE,
|
||||
initial,
|
||||
});
|
||||
dispatch(toggleDetailedLogs(false));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -149,7 +119,6 @@ const Logs = (props) => {
|
|||
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE));
|
||||
dispatch(getFilteringStatus());
|
||||
dispatch(getClients());
|
||||
try {
|
||||
|
@ -169,6 +138,7 @@ const Logs = (props) => {
|
|||
mediaQuery.removeEventListener('change', mediaQueryHandler);
|
||||
} catch (e1) {
|
||||
try {
|
||||
// Safari 13.1 do not support mediaQuery.addEventListener('change', handler)
|
||||
mediaQuery.removeListener(mediaQueryHandler);
|
||||
} catch (e2) {
|
||||
console.error(e2);
|
||||
|
@ -179,99 +149,53 @@ const Logs = (props) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const refreshLogs = async () => {
|
||||
setIsLoading(true);
|
||||
await Promise.all([
|
||||
dispatch(setLogsPage(TABLE_FIRST_PAGE)),
|
||||
dispatch(refreshFilteredLogs()),
|
||||
]);
|
||||
dispatch(addSuccessToast('query_log_updated'));
|
||||
setIsLoading(false);
|
||||
};
|
||||
const renderPage = () => <>
|
||||
<Filters
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
/>
|
||||
<InfiniteTable
|
||||
isLoading={isLoading}
|
||||
items={logs}
|
||||
isSmallScreen={isSmallScreen}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
/>
|
||||
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: '100%',
|
||||
height: 'fit-content',
|
||||
left: 0,
|
||||
top: 47,
|
||||
padding: '1rem 1.5rem 1rem',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
|
||||
onClick={closeModal}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</Modal>
|
||||
</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && (
|
||||
<>
|
||||
<Filters
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingAdditionalLogs={processingAdditionalLogs}
|
||||
refreshLogs={refreshLogs}
|
||||
/>
|
||||
<Table
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
logs={logs}
|
||||
pages={pages}
|
||||
page={page}
|
||||
autoClients={dashboard.autoClients}
|
||||
oldest={oldest}
|
||||
filtering={filtering}
|
||||
processingGetLogs={processingGetLogs}
|
||||
processingGetConfig={processingGetConfig}
|
||||
isDetailed={isDetailed}
|
||||
setLogsPagination={setLogsPagination}
|
||||
setLogsPage={setLogsPage}
|
||||
toggleDetailedLogs={toggleDetailedLogs}
|
||||
getLogs={getLogs}
|
||||
setRules={props.setRules}
|
||||
addSuccessToast={props.addSuccessToast}
|
||||
getFilteringStatus={props.getFilteringStatus}
|
||||
dnssec_enabled={dnsConfig.dnssec_enabled}
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: '100%',
|
||||
height: 'fit-content',
|
||||
left: 0,
|
||||
top: 47,
|
||||
padding: '1rem 1.5rem 1rem',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
|
||||
onClick={closeModal}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
{!enabled && !processingGetConfig && (
|
||||
<Disabled />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Logs.propTypes = {
|
||||
getLogs: PropTypes.func.isRequired,
|
||||
queryLogs: PropTypes.object.isRequired,
|
||||
dashboard: PropTypes.object.isRequired,
|
||||
getFilteringStatus: PropTypes.func.isRequired,
|
||||
filtering: PropTypes.object.isRequired,
|
||||
setRules: PropTypes.func.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
setLogsPagination: PropTypes.func.isRequired,
|
||||
setLogsPage: PropTypes.func.isRequired,
|
||||
toggleDetailedLogs: PropTypes.func.isRequired,
|
||||
dnsConfig: PropTypes.object.isRequired,
|
||||
return <>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && renderPage()}
|
||||
{!enabled && !processingGetConfig && <Disabled />}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
|
|
|
@ -77,12 +77,12 @@ const StaticLeases = ({
|
|||
title={t('delete_table_action')}
|
||||
disabled={processingDeleting}
|
||||
onClick={() => handleDelete(ip, mac, hostname)}
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>;
|
||||
>
|
||||
<svg className="icons">
|
||||
<use xlinkHref="#delete"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>;
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
|
|
@ -1,71 +1,50 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { FAILURE_TOAST_TIMEOUT, SUCCESS_TOAST_TIMEOUT } from '../../helpers/constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { TOAST_TIMEOUTS } from '../../helpers/constants';
|
||||
import { removeToast } from '../../actions';
|
||||
|
||||
class Toast extends Component {
|
||||
state = {
|
||||
timerId: null,
|
||||
const Toast = ({
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const [timerId, setTimerId] = useState(null);
|
||||
|
||||
const clearRemoveToastTimeout = () => clearTimeout(timerId);
|
||||
const removeCurrentToast = () => dispatch(removeToast(id));
|
||||
const setRemoveToastTimeout = () => {
|
||||
const timeout = TOAST_TIMEOUTS[type];
|
||||
const timerId = setTimeout(removeCurrentToast, timeout);
|
||||
|
||||
setTimerId(timerId);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.setRemoveToastTimeout();
|
||||
}
|
||||
useEffect(() => {
|
||||
setRemoveToastTimeout();
|
||||
}, []);
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearRemoveToastTimeout = () => clearTimeout(this.state.timerId);
|
||||
|
||||
setRemoveToastTimeout = () => {
|
||||
const timeout = this.props.type === 'success' ? SUCCESS_TOAST_TIMEOUT : FAILURE_TOAST_TIMEOUT;
|
||||
|
||||
const timerId = setTimeout(() => {
|
||||
this.props.removeToast(this.props.id);
|
||||
}, timeout);
|
||||
|
||||
this.setState({ timerId });
|
||||
};
|
||||
|
||||
showMessage(t, type, message) {
|
||||
if (type === 'notice') {
|
||||
return <span dangerouslySetInnerHTML={{ __html: t(message) }} />;
|
||||
}
|
||||
|
||||
return <Trans>{message}</Trans>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
type, id, t, message,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={`toast toast--${type}`}
|
||||
onMouseOver={this.clearRemoveToastTimeout}
|
||||
onMouseOut={this.setRemoveToastTimeout}>
|
||||
<p className="toast__content">
|
||||
{this.showMessage(t, type, message)}
|
||||
</p>
|
||||
<button className="toast__dismiss" onClick={() => this.props.removeToast(id)}>
|
||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18 6-12 12" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div className={`toast toast--${type}`}
|
||||
onMouseOver={clearRemoveToastTimeout}
|
||||
onMouseOut={setRemoveToastTimeout}>
|
||||
<p className="toast__content">{t(message)}</p>
|
||||
<button className="toast__dismiss" onClick={removeCurrentToast}>
|
||||
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m18 6-12 12" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
Toast.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
removeToast: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(Toast);
|
||||
export default Toast;
|
||||
|
|
|
@ -1,41 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector, shallowEqual } from 'react-redux';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
import * as actionCreators from '../../actions';
|
||||
import { TOAST_TRANSITION_TIMEOUT } from '../../helpers/constants';
|
||||
import Toast from './Toast';
|
||||
|
||||
import './Toast.css';
|
||||
|
||||
const Toasts = (props) => (
|
||||
<TransitionGroup className="toasts">
|
||||
{props.toasts.notices?.map((toast) => {
|
||||
const { id } = toast;
|
||||
return (
|
||||
<CSSTransition
|
||||
key={id}
|
||||
timeout={500}
|
||||
classNames="toast"
|
||||
>
|
||||
<Toast removeToast={props.removeToast} {...toast} />
|
||||
</CSSTransition>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
);
|
||||
const Toasts = () => {
|
||||
const toasts = useSelector((state) => state.toasts, shallowEqual);
|
||||
|
||||
Toasts.propTypes = {
|
||||
toasts: PropTypes.object,
|
||||
removeToast: PropTypes.func,
|
||||
return <TransitionGroup className="toasts">
|
||||
{toasts.notices?.map((toast) => {
|
||||
const { id } = toast;
|
||||
return <CSSTransition
|
||||
key={id}
|
||||
timeout={TOAST_TRANSITION_TIMEOUT}
|
||||
classNames="toast"
|
||||
>
|
||||
<Toast {...toast} />
|
||||
</CSSTransition>;
|
||||
})}
|
||||
</TransitionGroup>;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { toasts } = state;
|
||||
const props = { toasts };
|
||||
return props;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
actionCreators,
|
||||
)(Toasts);
|
||||
export default Toasts;
|
||||
|
|
|
@ -118,14 +118,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.card .red {
|
||||
.card .logs__cell--red {
|
||||
background-color: #fff4f2;
|
||||
}
|
||||
|
||||
.card .green {
|
||||
.card .logs__cell--green {
|
||||
background-color: #f1faf3;
|
||||
}
|
||||
|
||||
.card .blue {
|
||||
.card .logs__row--blue {
|
||||
background-color: #ecf7ff;
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
z-index: 100;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
opacity: 0.8;
|
||||
background-color: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.loading:after {
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import './Loading.css';
|
||||
|
||||
const Loading = ({ className }) => (
|
||||
<div className={classNames('loading', className)} />
|
||||
);
|
||||
const Loading = ({ className, text }) => {
|
||||
const { t } = useTranslation();
|
||||
return <div className={classNames('loading', className)}>{t(text)}</div>;
|
||||
};
|
||||
|
||||
Loading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
|
|
@ -13,18 +13,18 @@
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.rt-tr-group.red {
|
||||
.rt-tr-group.logs__row--red {
|
||||
background-color: rgba(223, 56, 18, 0.05);
|
||||
}
|
||||
|
||||
.rt-tr-group.green {
|
||||
.rt-tr-group.logs__row--green {
|
||||
background-color: rgba(103, 178, 121, 0.1);
|
||||
}
|
||||
|
||||
.rt-tr-group.blue {
|
||||
.rt-tr-group.logs__row--blue {
|
||||
background-color: #e5effd;
|
||||
}
|
||||
|
||||
.rt-tr-group.yellow {
|
||||
.rt-tr-group.logs__row--yellow {
|
||||
background-color: var(--yellow-pale);
|
||||
}
|
||||
|
|
|
@ -34,33 +34,47 @@ const Tooltip = ({
|
|||
delayShowValue = 0;
|
||||
}
|
||||
|
||||
const renderTooltip = ({ tooltipRef, getTooltipProps }) => (
|
||||
<div
|
||||
{...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className,
|
||||
})}
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTrigger = ({ getTriggerProps, triggerRef }) => (
|
||||
<span
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
className: triggerClass,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
renderTooltip.propTypes = {
|
||||
tooltipRef: propTypes.object,
|
||||
getTooltipProps: propTypes.func,
|
||||
};
|
||||
|
||||
renderTrigger.propTypes = {
|
||||
triggerRef: propTypes.object,
|
||||
getTriggerProps: propTypes.func,
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipTrigger
|
||||
placement={placement}
|
||||
trigger={triggerValue}
|
||||
delayHide={delayHideValue}
|
||||
delayShow={delayShowValue}
|
||||
tooltip={({ tooltipRef, getTooltipProps }) => (
|
||||
<div
|
||||
{...getTooltipProps({
|
||||
ref: tooltipRef,
|
||||
className,
|
||||
})}
|
||||
>
|
||||
{typeof content === 'string' ? t(content) : content}
|
||||
</div>
|
||||
)}
|
||||
tooltip={renderTooltip}
|
||||
>
|
||||
{({ getTriggerProps, triggerRef }) => (
|
||||
<span
|
||||
{...getTriggerProps({
|
||||
ref: triggerRef,
|
||||
className: triggerClass,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
{renderTrigger}
|
||||
</TooltipTrigger>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { toggleProtection, getClients } from '../actions';
|
||||
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
|
||||
import { toggleClientBlock, getAccessList } from '../actions/access';
|
||||
import { getAccessList } from '../actions/access';
|
||||
import Dashboard from '../components/Dashboard';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
|
@ -16,7 +16,6 @@ const mapDispatchToProps = {
|
|||
getStats,
|
||||
getStatsConfig,
|
||||
setStatsConfig,
|
||||
toggleClientBlock,
|
||||
getAccessList,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { getFilteringStatus, setRules } from '../actions/filtering';
|
||||
import {
|
||||
getLogs, setLogsPagination, setLogsPage, toggleDetailedLogs,
|
||||
} from '../actions/queryLogs';
|
||||
import Logs from '../components/Logs';
|
||||
import { addSuccessToast } from '../actions/toasts';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {
|
||||
queryLogs, dashboard, filtering, dnsConfig,
|
||||
} = state;
|
||||
|
||||
const props = {
|
||||
queryLogs,
|
||||
dashboard,
|
||||
filtering,
|
||||
dnsConfig,
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
getLogs,
|
||||
getFilteringStatus,
|
||||
setRules,
|
||||
addSuccessToast,
|
||||
setLogsPagination,
|
||||
setLogsPage,
|
||||
toggleDetailedLogs,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Logs);
|
|
@ -307,9 +307,7 @@ export const DEFAULT_LOGS_FILTER = {
|
|||
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
export const TABLE_DEFAULT_PAGE_SIZE = 25;
|
||||
|
||||
export const TABLE_FIRST_PAGE = 0;
|
||||
export const QUERY_LOGS_PAGE_LIMIT = 20;
|
||||
|
||||
export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
|
@ -327,85 +325,93 @@ export const FILTERED_STATUS = {
|
|||
|
||||
export const RESPONSE_FILTER = {
|
||||
ALL: {
|
||||
query: 'all',
|
||||
label: 'all_queries',
|
||||
QUERY: 'all',
|
||||
LABEL: 'all_queries',
|
||||
},
|
||||
FILTERED: {
|
||||
query: 'filtered',
|
||||
label: 'filtered',
|
||||
QUERY: 'filtered',
|
||||
LABEL: 'filtered',
|
||||
},
|
||||
PROCESSED: {
|
||||
query: 'processed',
|
||||
label: 'show_processed_responses',
|
||||
QUERY: 'processed',
|
||||
LABEL: 'show_processed_responses',
|
||||
},
|
||||
BLOCKED: {
|
||||
query: 'blocked',
|
||||
label: 'show_blocked_responses',
|
||||
QUERY: 'blocked',
|
||||
LABEL: 'show_blocked_responses',
|
||||
},
|
||||
BLOCKED_THREATS: {
|
||||
query: 'blocked_safebrowsing',
|
||||
label: 'blocked_threats',
|
||||
QUERY: 'blocked_safebrowsing',
|
||||
LABEL: 'blocked_threats',
|
||||
},
|
||||
BLOCKED_ADULT_WEBSITES: {
|
||||
query: 'blocked_parental',
|
||||
label: 'blocked_adult_websites',
|
||||
QUERY: 'blocked_parental',
|
||||
LABEL: 'blocked_adult_websites',
|
||||
},
|
||||
ALLOWED: {
|
||||
query: 'whitelisted',
|
||||
label: 'allowed',
|
||||
QUERY: 'whitelisted',
|
||||
LABEL: 'allowed',
|
||||
},
|
||||
REWRITTEN: {
|
||||
query: 'rewritten',
|
||||
label: 'rewritten',
|
||||
QUERY: 'rewritten',
|
||||
LABEL: 'rewritten',
|
||||
},
|
||||
SAFE_SEARCH: {
|
||||
query: 'safe_search',
|
||||
label: 'safe_search',
|
||||
QUERY: 'safe_search',
|
||||
LABEL: 'safe_search',
|
||||
},
|
||||
};
|
||||
|
||||
export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER)
|
||||
.reduce((acc, { query }) => {
|
||||
acc[query] = query;
|
||||
.reduce((acc, { QUERY }) => {
|
||||
acc[QUERY] = QUERY;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const QUERY_STATUS_COLORS = {
|
||||
BLUE: 'blue',
|
||||
GREEN: 'green',
|
||||
RED: 'red',
|
||||
WHITE: 'white',
|
||||
YELLOW: 'yellow',
|
||||
};
|
||||
|
||||
export const FILTERED_STATUS_TO_META_MAP = {
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
|
||||
label: RESPONSE_FILTER.ALLOWED.label,
|
||||
color: 'green',
|
||||
LABEL: RESPONSE_FILTER.ALLOWED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.GREEN,
|
||||
},
|
||||
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
|
||||
label: RESPONSE_FILTER.PROCESSED.label,
|
||||
color: 'white',
|
||||
LABEL: RESPONSE_FILTER.PROCESSED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.WHITE,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
|
||||
label: RESPONSE_FILTER.BLOCKED.label,
|
||||
color: 'red',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.RED,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
|
||||
label: RESPONSE_FILTER.SAFE_SEARCH.label,
|
||||
color: 'yellow',
|
||||
LABEL: RESPONSE_FILTER.SAFE_SEARCH.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: {
|
||||
label: RESPONSE_FILTER.BLOCKED.label,
|
||||
color: 'red',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.RED,
|
||||
},
|
||||
[FILTERED_STATUS.REWRITE]: {
|
||||
label: RESPONSE_FILTER.REWRITTEN.label,
|
||||
color: 'blue',
|
||||
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||
},
|
||||
[FILTERED_STATUS.REWRITE_HOSTS]: {
|
||||
label: RESPONSE_FILTER.REWRITTEN.label,
|
||||
color: 'blue',
|
||||
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: {
|
||||
label: RESPONSE_FILTER.BLOCKED_THREATS.label,
|
||||
color: 'yellow',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_PARENTAL]: {
|
||||
label: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.label,
|
||||
color: 'yellow',
|
||||
LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -519,3 +525,17 @@ export const DHCP_DESCRIPTION_PLACEHOLDERS = {
|
|||
lease_duration: 'dhcp_form_lease_input',
|
||||
},
|
||||
};
|
||||
|
||||
export const TOAST_TRANSITION_TIMEOUT = 500;
|
||||
|
||||
export const TOAST_TYPES = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
NOTICE: 'notice',
|
||||
};
|
||||
|
||||
export const TOAST_TIMEOUTS = {
|
||||
[TOAST_TYPES.SUCCESS]: 5000,
|
||||
[TOAST_TYPES.ERROR]: 30000,
|
||||
[TOAST_TYPES.NOTICE]: 30000,
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import { getTrackerData } from './trackers/trackers';
|
|||
|
||||
import {
|
||||
CHECK_TIMEOUT,
|
||||
CUSTOM_FILTERING_RULES_ID,
|
||||
DEFAULT_DATE_FORMAT_OPTIONS,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
|
@ -742,6 +743,30 @@ export const sortIp = (a, b) => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {array} filters
|
||||
* @param {array} whitelistFilters
|
||||
* @param {number} filterId
|
||||
* @param {function} t - translate
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getFilterName = (
|
||||
filters,
|
||||
whitelistFilters,
|
||||
filterId,
|
||||
customFilterTranslationKey = 'custom_filter_rules',
|
||||
resolveFilterName = (filter) => (filter ? filter.name : i18n.t('unknown_filter', { filterId })),
|
||||
) => {
|
||||
if (filterId === CUSTOM_FILTERING_RULES_ID) {
|
||||
return i18n.t(customFilterTranslationKey);
|
||||
}
|
||||
|
||||
const matchIdPredicate = (filter) => filter.id === filterId;
|
||||
const filter = filters.find(matchIdPredicate) || whitelistFilters.find(matchIdPredicate);
|
||||
|
||||
return resolveFilterName(filter);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param ip {string}
|
||||
* @param gateway_ip {string}
|
||||
|
@ -803,3 +828,11 @@ export const enrichWithConcatenatedIpAddresses = (interfaces) => Object.entries(
|
|||
acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses);
|
||||
return acc;
|
||||
}, interfaces);
|
||||
|
||||
export const isScrolledIntoView = (el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const elemTop = rect.top;
|
||||
const elemBottom = rect.bottom;
|
||||
|
||||
return elemTop < window.innerHeight && elemBottom >= 0;
|
||||
};
|
||||
|
|
|
@ -24,9 +24,17 @@ const getFormattedWhois = (whois) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
|
||||
const { value, original: { info } } = row;
|
||||
let whoisContainer = '';
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {object} info
|
||||
* @param {string} info.name
|
||||
* @param {object} info.whois_info
|
||||
* @param {boolean} [isDetailed]
|
||||
* @param {boolean} [isLogs]
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => {
|
||||
let whoisContainer = null;
|
||||
let nameContainer = value;
|
||||
|
||||
if (info) {
|
||||
|
@ -34,42 +42,28 @@ export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
|
|||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
|
||||
if (name) {
|
||||
if (isLogs) {
|
||||
nameContainer = !whoisAvailable && isDetailed
|
||||
? (
|
||||
<small title={value}>{value}</small>
|
||||
) : (
|
||||
<div className="logs__text logs__text--nowrap" title={`${name} (${value})`}>
|
||||
{name} <small>{`(${value})`}</small>
|
||||
</div>
|
||||
);
|
||||
const nameValue = <div className="logs__text logs__text--nowrap" title={`${name} (${value})`}>
|
||||
{name} <small>{`(${value})`}</small>
|
||||
</div>;
|
||||
|
||||
if (!isLogs) {
|
||||
nameContainer = nameValue;
|
||||
} else {
|
||||
nameContainer = (
|
||||
<div
|
||||
className="logs__text logs__text--nowrap"
|
||||
title={`${name} (${value})`}
|
||||
>
|
||||
{name} <small>{`(${value})`}</small>
|
||||
</div>
|
||||
);
|
||||
nameContainer = !whoisAvailable && isDetailed
|
||||
? <small title={value}>{value}</small>
|
||||
: nameValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (whoisAvailable && isDetailed) {
|
||||
whoisContainer = (
|
||||
<div className="logs__text logs__text--wrap logs__text--whois">
|
||||
whoisContainer = <div className="logs__text logs__text--wrap logs__text--whois">
|
||||
{getFormattedWhois(whois_info)}
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs__text mw-100" title={value}>
|
||||
<>
|
||||
{nameContainer}
|
||||
{whoisContainer}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
return <div className="logs__text mw-100" title={value}>
|
||||
{nameContainer}
|
||||
{whoisContainer}
|
||||
</div>;
|
||||
};
|
|
@ -1,30 +1,10 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
|
||||
import * as actions from '../actions/queryLogs';
|
||||
import { DEFAULT_LOGS_FILTER, TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants';
|
||||
import { DEFAULT_LOGS_FILTER } from '../helpers/constants';
|
||||
|
||||
const queryLogs = handleActions(
|
||||
{
|
||||
[actions.setLogsPagination]: (state, { payload }) => {
|
||||
const { page, pageSize } = payload;
|
||||
const { allLogs } = state;
|
||||
const rowsStart = pageSize * page;
|
||||
const rowsEnd = (pageSize * page) + pageSize;
|
||||
const logsSlice = allLogs.slice(rowsStart, rowsEnd);
|
||||
const pages = Math.ceil(allLogs.length / pageSize);
|
||||
|
||||
return {
|
||||
...state,
|
||||
pages,
|
||||
logs: logsSlice,
|
||||
};
|
||||
},
|
||||
|
||||
[actions.setLogsPage]: (state, { payload }) => ({
|
||||
...state,
|
||||
page: payload,
|
||||
}),
|
||||
|
||||
[actions.setFilteredLogsRequest]: (state) => ({ ...state, processingGetLogs: true }),
|
||||
[actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||
[actions.toggleDetailedLogs]: (state, { payload }) => ({
|
||||
|
@ -34,14 +14,7 @@ const queryLogs = handleActions(
|
|||
|
||||
[actions.setFilteredLogsSuccess]: (state, { payload }) => {
|
||||
const { logs, oldest, filter } = payload;
|
||||
const pageSize = TABLE_DEFAULT_PAGE_SIZE;
|
||||
const page = 0;
|
||||
|
||||
const pages = Math.ceil(logs.length / pageSize);
|
||||
const total = logs.length;
|
||||
const rowsStart = pageSize * page;
|
||||
const rowsEnd = rowsStart + pageSize;
|
||||
const logsSlice = logs.slice(rowsStart, rowsEnd);
|
||||
const isFiltered = filter && Object.keys(filter).some((key) => filter[key]);
|
||||
|
||||
return {
|
||||
|
@ -49,10 +22,8 @@ const queryLogs = handleActions(
|
|||
oldest,
|
||||
filter,
|
||||
isFiltered,
|
||||
pages,
|
||||
total,
|
||||
logs: logsSlice,
|
||||
allLogs: logs,
|
||||
logs,
|
||||
isEntireLog: logs.length < 1,
|
||||
processingGetLogs: false,
|
||||
};
|
||||
},
|
||||
|
@ -67,29 +38,13 @@ const queryLogs = handleActions(
|
|||
[actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
|
||||
[actions.getLogsSuccess]: (state, { payload }) => {
|
||||
const {
|
||||
logs, oldest, older_than, page, pageSize, initial,
|
||||
logs, oldest, older_than,
|
||||
} = payload;
|
||||
let logsWithOffset = state.allLogs.length > 0 && !initial ? state.allLogs : logs;
|
||||
let allLogs = logs;
|
||||
|
||||
if (older_than) {
|
||||
logsWithOffset = [...state.allLogs, ...logs];
|
||||
allLogs = [...state.allLogs, ...logs];
|
||||
}
|
||||
|
||||
const pages = Math.ceil(logsWithOffset.length / pageSize);
|
||||
const total = logsWithOffset.length;
|
||||
const rowsStart = pageSize * page;
|
||||
const rowsEnd = (pageSize * page) + pageSize;
|
||||
const logsSlice = logsWithOffset.slice(rowsStart, rowsEnd);
|
||||
|
||||
return {
|
||||
...state,
|
||||
oldest,
|
||||
pages,
|
||||
total,
|
||||
allLogs,
|
||||
logs: logsSlice,
|
||||
logs: older_than ? [...state.logs, ...logs] : logs,
|
||||
isEntireLog: logs.length < 1,
|
||||
processingGetLogs: false,
|
||||
};
|
||||
|
@ -126,7 +81,7 @@ const queryLogs = handleActions(
|
|||
...state, processingAdditionalLogs: false, processingGetLogs: false,
|
||||
}),
|
||||
[actions.getAdditionalLogsSuccess]: (state) => ({
|
||||
...state, processingAdditionalLogs: false, processingGetLogs: false,
|
||||
...state, processingAdditionalLogs: false, processingGetLogs: false, isEntireLog: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
@ -135,18 +90,15 @@ const queryLogs = handleActions(
|
|||
processingGetConfig: false,
|
||||
processingSetConfig: false,
|
||||
processingAdditionalLogs: false,
|
||||
logs: [],
|
||||
interval: 1,
|
||||
allLogs: [],
|
||||
page: 0,
|
||||
pages: 0,
|
||||
total: 0,
|
||||
logs: [],
|
||||
enabled: true,
|
||||
oldest: '',
|
||||
filter: DEFAULT_LOGS_FILTER,
|
||||
isFiltered: false,
|
||||
anonymize_client_ip: false,
|
||||
isDetailed: true,
|
||||
isEntireLog: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -5,16 +5,17 @@ import {
|
|||
addErrorToast, addNoticeToast, addSuccessToast,
|
||||
} from '../actions/toasts';
|
||||
import { removeToast } from '../actions';
|
||||
import { TOAST_TYPES } from '../helpers/constants';
|
||||
|
||||
const toasts = handleActions({
|
||||
[addErrorToast]: (state, { payload }) => {
|
||||
const message = payload.error.toString();
|
||||
console.error(message);
|
||||
console.error(payload.error);
|
||||
|
||||
const errorToast = {
|
||||
id: nanoid(),
|
||||
message,
|
||||
type: 'error',
|
||||
type: TOAST_TYPES.ERROR,
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, errorToast] };
|
||||
|
@ -24,7 +25,7 @@ const toasts = handleActions({
|
|||
const successToast = {
|
||||
id: nanoid(),
|
||||
message: payload,
|
||||
type: 'success',
|
||||
type: TOAST_TYPES.SUCCESS,
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, successToast] };
|
||||
|
@ -34,7 +35,7 @@ const toasts = handleActions({
|
|||
const noticeToast = {
|
||||
id: nanoid(),
|
||||
message: payload.error.toString(),
|
||||
type: 'notice',
|
||||
type: TOAST_TYPES.NOTICE,
|
||||
};
|
||||
|
||||
const newState = { ...state, notices: [...state.notices, noticeToast] };
|
||||
|
|
5
client/webpack.common.js
vendored
5
client/webpack.common.js
vendored
|
@ -41,9 +41,8 @@ const config = {
|
|||
alias: {
|
||||
MainRoot: path.resolve(__dirname, '../'),
|
||||
ClientRoot: path.resolve(__dirname, './src'),
|
||||
// TODO: change to '@hot-loader/react-dom' when v16.13.1 is released
|
||||
// https://stackoverflow.com/a/62671689/12942752
|
||||
'react-dom': 'react-dom',
|
||||
// TODO: uncomment when v16.13.1 is released https://stackoverflow.com/a/62671689/12942752
|
||||
// 'react-dom': '@hot-loader/react-dom',
|
||||
},
|
||||
},
|
||||
module: {
|
||||
|
|
|
@ -165,13 +165,12 @@ func (l *queryLog) parseSearchParams(r *http.Request) (*searchParams, error) {
|
|||
|
||||
if limit, err := strconv.ParseInt(q.Get("limit"), 10, 64); err == nil {
|
||||
p.limit = int(limit)
|
||||
|
||||
// If limit or offset are specified explicitly, we should change the default behavior
|
||||
// and scan all log records until we found enough log entries
|
||||
p.maxFileScanEntries = 0
|
||||
}
|
||||
if offset, err := strconv.ParseInt(q.Get("offset"), 10, 64); err == nil {
|
||||
p.offset = int(offset)
|
||||
|
||||
// If we don't use "olderThan" and use offset/limit instead, we should change the default behavior
|
||||
// and scan all log records until we found enough log entries
|
||||
p.maxFileScanEntries = 0
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue