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:
Artem Baskal 2020-09-01 16:30:30 +03:00
parent 0a4781be97
commit 6b61429572
74 changed files with 1449 additions and 1861 deletions

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

@ -1356,17 +1356,6 @@
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" "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": { "@istanbuljs/load-nyc-config": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -10784,6 +10773,24 @@
"yallist": "^4.0.0" "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": { "mixin-deep": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
@ -12169,9 +12176,9 @@
} }
}, },
"pump": { "pump": {
"version": "2.0.1", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true, "dev": true,
"requires": { "requires": {
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
@ -12187,6 +12194,18 @@
"duplexify": "^3.6.0", "duplexify": "^3.6.0",
"inherits": "^2.0.3", "inherits": "^2.0.3",
"pump": "^2.0.0" "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": { "punycode": {
@ -13281,10 +13300,13 @@
} }
}, },
"serialize-javascript": { "serialize-javascript": {
"version": "3.0.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
"integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
"dev": true "dev": true,
"requires": {
"randombytes": "^2.1.0"
}
}, },
"serve-index": { "serve-index": {
"version": "1.9.1", "version": "1.9.1",
@ -14619,16 +14641,16 @@
} }
}, },
"terser-webpack-plugin": { "terser-webpack-plugin": {
"version": "1.4.3", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
"dev": true, "dev": true,
"requires": { "requires": {
"cacache": "^12.0.2", "cacache": "^12.0.2",
"find-cache-dir": "^2.1.0", "find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0", "is-wsl": "^1.1.0",
"schema-utils": "^1.0.0", "schema-utils": "^1.0.0",
"serialize-javascript": "^2.1.2", "serialize-javascript": "^4.0.0",
"source-map": "^0.6.1", "source-map": "^0.6.1",
"terser": "^4.1.2", "terser": "^4.1.2",
"webpack-sources": "^1.4.0", "webpack-sources": "^1.4.0",
@ -14688,15 +14710,6 @@
"path-exists": "^3.0.0" "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": { "make-dir": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@ -14707,24 +14720,6 @@
"semver": "^5.6.0" "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": { "p-limit": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -14764,22 +14759,15 @@
"find-up": "^3.0.0" "find-up": "^3.0.0"
} }
}, },
"pump": { "serialize-javascript": {
"version": "3.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true, "dev": true,
"requires": { "requires": {
"end-of-stream": "^1.1.0", "randombytes": "^2.1.0"
"once": "^1.3.1"
} }
}, },
"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": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -14794,12 +14782,6 @@
"requires": { "requires": {
"figgy-pudding": "^3.5.1" "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
View File

@ -13,7 +13,6 @@
"test:watch": "jest --watch" "test:watch": "jest --watch"
}, },
"dependencies": { "dependencies": {
"@hot-loader/react-dom": "^16.13.0",
"@nivo/line": "^0.49.1", "@nivo/line": "^0.49.1",
"axios": "^0.19.2", "axios": "^0.19.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",

View File

@ -139,8 +139,8 @@
"page_table_footer_text": "Страница", "page_table_footer_text": "Страница",
"rows_table_footer_text": "редове", "rows_table_footer_text": "редове",
"updated_custom_filtering_toast": "Обновени местни правила за филтриране", "updated_custom_filtering_toast": "Обновени местни правила за филтриране",
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране", "rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране: {{rule}}",
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране", "rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
"plain_dns": "Обикновен DNS", "plain_dns": "Обикновен DNS",
"source_label": "Източник", "source_label": "Източник",
"found_in_known_domain_db": "Намерен в списъците с домейни.", "found_in_known_domain_db": "Намерен в списъците с домейни.",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Stránka", "page_table_footer_text": "Stránka",
"rows_table_footer_text": "řádky", "rows_table_footer_text": "řádky",
"updated_custom_filtering_toast": "Aktualizovaná vlastní pravidla filtrování", "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_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_added_to_custom_filtering_toast": "Pravidlo přidáno do vlastních pravidel filtrování: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrováno pomocí {{filter}}", "query_log_filtered": "Filtrováno pomocí {{filter}}",
"query_log_confirm_clear": "Opravdu chcete vymazat celý protokol dotazů?", "query_log_confirm_clear": "Opravdu chcete vymazat celý protokol dotazů?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Side", "page_table_footer_text": "Side",
"rows_table_footer_text": "rækker", "rows_table_footer_text": "rækker",
"updated_custom_filtering_toast": "De brugerdefinerede filtreringsregler er blevet opdateret", "updated_custom_filtering_toast": "De brugerdefinerede filtreringsregler er blevet opdateret",
"rule_removed_from_custom_filtering_toast": "Regel fjernet fra 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_added_to_custom_filtering_toast": "Regel tilføjet til de brugerdefinerede filtreringsregler: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtreret af {{filter}}", "query_log_filtered": "Filtreret af {{filter}}",
"query_log_confirm_clear": "Er du sikker på, at du vil rydde hele forespørgselsloggen?", "query_log_confirm_clear": "Er du sikker på, at du vil rydde hele forespørgselsloggen?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Seite", "page_table_footer_text": "Seite",
"rows_table_footer_text": "Reihen", "rows_table_footer_text": "Reihen",
"updated_custom_filtering_toast": "Die benutzerdefinierten Filterregeln wurden aktualisiert", "updated_custom_filtering_toast": "Die benutzerdefinierten Filterregeln wurden aktualisiert",
"rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt", "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_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Gefiltert nach {{filter}}", "query_log_filtered": "Gefiltert nach {{filter}}",
"query_log_confirm_clear": "Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?", "query_log_confirm_clear": "Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?",

View File

@ -213,8 +213,8 @@
"page_table_footer_text": "Page", "page_table_footer_text": "Page",
"rows_table_footer_text": "rows", "rows_table_footer_text": "rows",
"updated_custom_filtering_toast": "Updated the custom filtering rules", "updated_custom_filtering_toast": "Updated the custom filtering rules",
"rule_removed_from_custom_filtering_toast": "Rule removed from 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_added_to_custom_filtering_toast": "Rule added to the custom filtering rules: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtered by {{filter}}", "query_log_filtered": "Filtered by {{filter}}",
"query_log_confirm_clear": "Are you sure you want to clear the entire query log?", "query_log_confirm_clear": "Are you sure you want to clear the entire query log?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Página", "page_table_footer_text": "Página",
"rows_table_footer_text": "filas", "rows_table_footer_text": "filas",
"updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas", "updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas",
"rule_removed_from_custom_filtering_toast": "Regla eliminada de 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_added_to_custom_filtering_toast": "Regla añadida a las reglas de filtrado personalizado: {{rule}}",
"query_log_response_status": "Estado: {{value}}", "query_log_response_status": "Estado: {{value}}",
"query_log_filtered": "Filtrado por {{filter}}", "query_log_filtered": "Filtrado por {{filter}}",
"query_log_confirm_clear": "¿Está seguro de que desea borrar todo el registro de consultas?", "query_log_confirm_clear": "¿Está seguro de que desea borrar todo el registro de consultas?",

View File

@ -202,8 +202,8 @@
"page_table_footer_text": "صفحه", "page_table_footer_text": "صفحه",
"rows_table_footer_text": "سطر", "rows_table_footer_text": "سطر",
"updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است", "updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است",
"rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد", "rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد {{rule}}",
"rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد", "rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد {{rule}}",
"query_log_response_status": "وضعیت: {{value}}", "query_log_response_status": "وضعیت: {{value}}",
"query_log_filtered": "فیلتر شده با {{filter}}", "query_log_filtered": "فیلتر شده با {{filter}}",
"query_log_confirm_clear": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟", "query_log_confirm_clear": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Page", "page_table_footer_text": "Page",
"rows_table_footer_text": "lignes", "rows_table_footer_text": "lignes",
"updated_custom_filtering_toast": "Règles de filtrage d'utilisateur mises à jour", "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_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_added_to_custom_filtering_toast": "Règle ajoutée aux règles d'utilisateur: {{rule}}",
"query_log_response_status": "Statut : {{value}}", "query_log_response_status": "Statut : {{value}}",
"query_log_filtered": "Filtré par {{filter}}", "query_log_filtered": "Filtré par {{filter}}",
"query_log_confirm_clear": "Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?", "query_log_confirm_clear": "Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Stranica", "page_table_footer_text": "Stranica",
"rows_table_footer_text": "redova", "rows_table_footer_text": "redova",
"updated_custom_filtering_toast": "Ažurirana su prilagođena pravila filtriranja", "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_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_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrirao {{filter}}", "query_log_filtered": "Filtrirao {{filter}}",
"query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?", "query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Halaman", "page_table_footer_text": "Halaman",
"rows_table_footer_text": "baris", "rows_table_footer_text": "baris",
"updated_custom_filtering_toast": "Perbarui aturan penyaringan khusus", "updated_custom_filtering_toast": "Perbarui aturan penyaringan khusus",
"rule_removed_from_custom_filtering_toast": "Aturan dihapus dari 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_added_to_custom_filtering_toast": "Aturan ditambah ke aturan penyaringan khusus: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Difilter oleh {{filter}}", "query_log_filtered": "Difilter oleh {{filter}}",
"query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?", "query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Pagina", "page_table_footer_text": "Pagina",
"rows_table_footer_text": "righe", "rows_table_footer_text": "righe",
"updated_custom_filtering_toast": "Le regole dei filtri personalizzate sono state aggiornate", "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_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_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrato da {{filter}}", "query_log_filtered": "Filtrato da {{filter}}",
"query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?", "query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "ページ", "page_table_footer_text": "ページ",
"rows_table_footer_text": "行", "rows_table_footer_text": "行",
"updated_custom_filtering_toast": "カスタム・フィルタリングルールを更新しました", "updated_custom_filtering_toast": "カスタム・フィルタリングルールを更新しました",
"rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました", "rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました {{rule}}",
"rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました", "rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました {{rule}}",
"query_log_response_status": "ステータス: {{value}}", "query_log_response_status": "ステータス: {{value}}",
"query_log_filtered": "{{filter}}によるフィルタ", "query_log_filtered": "{{filter}}によるフィルタ",
"query_log_confirm_clear": "クエリ・ログ全体を消去してもよろしいですか?", "query_log_confirm_clear": "クエリ・ログ全体を消去してもよろしいですか?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "페이지", "page_table_footer_text": "페이지",
"rows_table_footer_text": "행", "rows_table_footer_text": "행",
"updated_custom_filtering_toast": "사용자 정의 필터링 규칙 업데이트", "updated_custom_filtering_toast": "사용자 정의 필터링 규칙 업데이트",
"rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거", "rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거 {{rule}}",
"rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙", "rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙 {{rule}}",
"query_log_response_status": "상태: {{value}}", "query_log_response_status": "상태: {{value}}",
"query_log_filtered": "필터: {{filter}}", "query_log_filtered": "필터: {{filter}}",
"query_log_confirm_clear": "정말로 모든 쿼리 로그를 비우시겠습니까?", "query_log_confirm_clear": "정말로 모든 쿼리 로그를 비우시겠습니까?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Pagina", "page_table_footer_text": "Pagina",
"rows_table_footer_text": "rijen", "rows_table_footer_text": "rijen",
"updated_custom_filtering_toast": "Aangepaste filter regels zijn bijgewerkt", "updated_custom_filtering_toast": "Aangepaste filter regels zijn bijgewerkt",
"rule_removed_from_custom_filtering_toast": "Regel verwijderd uit 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_added_to_custom_filtering_toast": "Regel toegevoegd aan de aangepaste filterregels: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Gefilterd door {{filter}}", "query_log_filtered": "Gefilterd door {{filter}}",
"query_log_confirm_clear": "Weet u zeker dat u het hele query logboek wilt legen?", "query_log_confirm_clear": "Weet u zeker dat u het hele query logboek wilt legen?",

View File

@ -207,8 +207,8 @@
"page_table_footer_text": "Side", "page_table_footer_text": "Side",
"rows_table_footer_text": "rekker", "rows_table_footer_text": "rekker",
"updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene", "updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene",
"rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra 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_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrert av {{filter}}", "query_log_filtered": "Filtrert av {{filter}}",
"query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?", "query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Strona", "page_table_footer_text": "Strona",
"rows_table_footer_text": "wierszy", "rows_table_footer_text": "wierszy",
"updated_custom_filtering_toast": "Zaktualizowano niestandardowe reguły filtrowania", "updated_custom_filtering_toast": "Zaktualizowano niestandardowe reguły filtrowania",
"rule_removed_from_custom_filtering_toast": "Reguła usunięta z 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_added_to_custom_filtering_toast": "Reguła dodana do niestandardowych reguł filtrowania: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrowane przez {{filter}}", "query_log_filtered": "Filtrowane przez {{filter}}",
"query_log_confirm_clear": "Czy na pewno chcesz wyczyścić cały dziennik zapytań?", "query_log_confirm_clear": "Czy na pewno chcesz wyczyścić cały dziennik zapytań?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Página", "page_table_footer_text": "Página",
"rows_table_footer_text": "linhas", "rows_table_footer_text": "linhas",
"updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas", "updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas",
"rule_removed_from_custom_filtering_toast": "Regra removida das 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_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrado por {{filter}}", "query_log_filtered": "Filtrado por {{filter}}",
"query_log_confirm_clear": "Você tem certeza que deseja limpar o registro de consulta?", "query_log_confirm_clear": "Você tem certeza que deseja limpar o registro de consulta?",

View File

@ -168,8 +168,8 @@
"page_table_footer_text": "Página", "page_table_footer_text": "Página",
"rows_table_footer_text": "linhas", "rows_table_footer_text": "linhas",
"updated_custom_filtering_toast": "Regras de filtragem personalizadas actualizadas", "updated_custom_filtering_toast": "Regras de filtragem personalizadas actualizadas",
"rule_removed_from_custom_filtering_toast": "Regra removida das 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_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrado por {{filter}}", "query_log_filtered": "Filtrado por {{filter}}",
"query_log_confirm_clear": "Tem a certeza de que deseja limpar todo o registo de consulta?", "query_log_confirm_clear": "Tem a certeza de que deseja limpar todo o registo de consulta?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Pagina", "page_table_footer_text": "Pagina",
"rows_table_footer_text": "linii", "rows_table_footer_text": "linii",
"updated_custom_filtering_toast": "Reguli personalizate de filtrare aduse la zi", "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_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_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate: {{rule}}",
"query_log_response_status": "Statut: {{value}}", "query_log_response_status": "Statut: {{value}}",
"query_log_filtered": "Filtrat de {{filter}}", "query_log_filtered": "Filtrat de {{filter}}",
"query_log_confirm_clear": "Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?", "query_log_confirm_clear": "Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Страница", "page_table_footer_text": "Страница",
"rows_table_footer_text": "строк", "rows_table_footer_text": "строк",
"updated_custom_filtering_toast": "Внесены изменения в пользовательские правила", "updated_custom_filtering_toast": "Внесены изменения в пользовательские правила",
"rule_removed_from_custom_filtering_toast": равило удалено из авторского списка правил фильтрации", "rule_removed_from_custom_filtering_toast": ользовательское правило удалено: {{rule}}",
"rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено", "rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено: {{rule}}",
"query_log_response_status": "Статус: {{value}}", "query_log_response_status": "Статус: {{value}}",
"query_log_filtered": "Отфильтровано с помощью {{filter}}", "query_log_filtered": "Отфильтровано с помощью {{filter}}",
"query_log_confirm_clear": "Вы уверены, что хотите очистить весь журнал запросов?", "query_log_confirm_clear": "Вы уверены, что хотите очистить весь журнал запросов?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Stránka", "page_table_footer_text": "Stránka",
"rows_table_footer_text": "riadky", "rows_table_footer_text": "riadky",
"updated_custom_filtering_toast": "Aktualizované vlastné filtračné pravidlá", "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_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_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel: {{rule}}",
"query_log_response_status": "Stav: {{value}}", "query_log_response_status": "Stav: {{value}}",
"query_log_filtered": "Vyfiltrované pomocou {{filter}}", "query_log_filtered": "Vyfiltrované pomocou {{filter}}",
"query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?", "query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Stran", "page_table_footer_text": "Stran",
"rows_table_footer_text": "vrstic", "rows_table_footer_text": "vrstic",
"updated_custom_filtering_toast": "Posodobljena pravila filtriranja po meri", "updated_custom_filtering_toast": "Posodobljena pravila filtriranja po meri",
"rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil 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_added_to_custom_filtering_toast": "Pravilo je dodano pravilom filtriranja po meri: {{rule}}",
"query_log_response_status": "Stanje: {{value}}", "query_log_response_status": "Stanje: {{value}}",
"query_log_filtered": "Filtriran z {{filter}}", "query_log_filtered": "Filtriran z {{filter}}",
"query_log_confirm_clear": "Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?", "query_log_confirm_clear": "Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "Stranica", "page_table_footer_text": "Stranica",
"rows_table_footer_text": "redovi", "rows_table_footer_text": "redovi",
"updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja", "updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja",
"rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih 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_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja: {{rule}}",
"query_log_response_status": "Stanje: {{value}}", "query_log_response_status": "Stanje: {{value}}",
"query_log_filtered": "Filtrirano od {{filter}}", "query_log_filtered": "Filtrirano od {{filter}}",
"query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?", "query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?",

View File

@ -162,8 +162,8 @@
"page_table_footer_text": "Sida", "page_table_footer_text": "Sida",
"rows_table_footer_text": "rader", "rows_table_footer_text": "rader",
"updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna", "updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna",
"rule_removed_from_custom_filtering_toast": "Regel borttagen från 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_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna: {{rule}}",
"query_log_response_status": "Status: {{value}}", "query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtrerat av {{filter}}", "query_log_filtered": "Filtrerat av {{filter}}",
"query_log_confirm_clear": "Är du säker på att du vill rensa hela förfrågningsloggen?", "query_log_confirm_clear": "Är du säker på att du vill rensa hela förfrågningsloggen?",

View File

@ -167,8 +167,8 @@
"page_table_footer_text": "หน้า", "page_table_footer_text": "หน้า",
"rows_table_footer_text": "ตาราง", "rows_table_footer_text": "ตาราง",
"updated_custom_filtering_toast": "อัปเดตกฎการกรองที่กำหนดเอง", "updated_custom_filtering_toast": "อัปเดตกฎการกรองที่กำหนดเอง",
"rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว", "rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว {{rule}}",
"rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว", "rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว {{rule}}",
"query_log_response_status": "สถานะ: {{value}}", "query_log_response_status": "สถานะ: {{value}}",
"query_log_filtered": "กรองโดย {{filter}}", "query_log_filtered": "กรองโดย {{filter}}",
"query_log_confirm_clear": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?", "query_log_confirm_clear": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?",

View File

@ -197,8 +197,8 @@
"page_table_footer_text": "Sayfa", "page_table_footer_text": "Sayfa",
"rows_table_footer_text": "satır", "rows_table_footer_text": "satır",
"updated_custom_filtering_toast": "İsteğe bağlı filtreleme kuralları güncellendi", "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_removed_from_custom_filtering_toast": "Özel filtreleme kurallarından kural kaldırıldı: {{rule}}",
"rule_added_to_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarına eklendi", "rule_added_to_custom_filtering_toast": "Özel filtreleme kurallarına kural eklendi: {{rule}}",
"query_log_response_status": "Durum: {{value}}", "query_log_response_status": "Durum: {{value}}",
"query_log_filtered": "{{filter}} tarafından filtrelendi", "query_log_filtered": "{{filter}} tarafından filtrelendi",
"query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?", "query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?",

View File

@ -172,8 +172,8 @@
"page_table_footer_text": "Trang", "page_table_footer_text": "Trang",
"rows_table_footer_text": "hàng", "rows_table_footer_text": "hàng",
"updated_custom_filtering_toast": "Đã cập nhật quy tắc lọc tuỳ chỉnh", "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_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_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_response_status": "Trạng thái: {{value}}",
"query_log_filtered": "Được lọc bởi {{filter}}", "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?", "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?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "页", "page_table_footer_text": "页",
"rows_table_footer_text": "行", "rows_table_footer_text": "行",
"updated_custom_filtering_toast": "自定义过滤规则已更新", "updated_custom_filtering_toast": "自定义过滤规则已更新",
"rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除", "rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除 {{rule}}",
"rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中", "rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中 {{rule}}",
"query_log_response_status": "状态: {{value}}", "query_log_response_status": "状态: {{value}}",
"query_log_filtered": "被 {{filter}} 过滤", "query_log_filtered": "被 {{filter}} 过滤",
"query_log_confirm_clear": "你确定想要清除全部查询日志吗?", "query_log_confirm_clear": "你确定想要清除全部查询日志吗?",

View File

@ -208,8 +208,8 @@
"page_table_footer_text": "頁面", "page_table_footer_text": "頁面",
"rows_table_footer_text": "列", "rows_table_footer_text": "列",
"updated_custom_filtering_toast": "已更新自訂的過濾規則", "updated_custom_filtering_toast": "已更新自訂的過濾規則",
"rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除", "rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除 {{rule}}",
"rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中", "rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中 {{rule}}",
"query_log_response_status": "狀態:{{value}}", "query_log_response_status": "狀態:{{value}}",
"query_log_filtered": "被 {{filter}} 過濾", "query_log_filtered": "被 {{filter}} 過濾",
"query_log_confirm_clear": "您確定您想要清除整個查詢記錄嗎?", "query_log_confirm_clear": "您確定您想要清除整個查詢記錄嗎?",

View File

@ -2,14 +2,17 @@ import { createAction } from 'redux-actions';
import i18next from 'i18next'; import i18next from 'i18next';
import axios from 'axios'; import axios from 'axios';
import endsWith from 'lodash/endsWith';
import escapeRegExp from 'lodash/escapeRegExp';
import { splitByNewLine, sortClients } from '../helpers/helpers'; import { splitByNewLine, sortClients } from '../helpers/helpers';
import { import {
CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME,
} from '../helpers/constants'; } from '../helpers/constants';
import { areEqualVersions } from '../helpers/version'; import { areEqualVersions } from '../helpers/version';
import { getTlsStatus } from './encryption'; import { getTlsStatus } from './encryption';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts'; import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts';
import { getFilteringStatus, setRules } from './filtering';
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE'); export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW'); 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 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());
};

View File

@ -3,9 +3,7 @@ import { createAction } from 'redux-actions';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers'; import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers';
import { import {
DEFAULT_LOGS_FILTER, DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT,
TABLE_DEFAULT_PAGE_SIZE,
TABLE_FIRST_PAGE,
} from '../helpers/constants'; } from '../helpers/constants';
import { addErrorToast, addSuccessToast } from './toasts'; 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 getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS'); 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 { logs, oldest } = data;
const totalData = total || { logs }; const totalData = total || { logs };
const needToGetAdditionalLogs = (logs.length < TABLE_DEFAULT_PAGE_SIZE const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
|| totalData.logs.length < TABLE_DEFAULT_PAGE_SIZE) const currentQuery = queryForm && queryForm.values.search;
&& oldest !== ''; 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()); dispatch(getAdditionalLogsRequest());
try { try {
@ -54,7 +59,7 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
filter, filter,
}); });
if (additionalLogs.oldest.length > 0) { if (additionalLogs.oldest.length > 0) {
return await checkFilteredLogs(additionalLogs, filter, dispatch, { return await shortPollQueryLogs(additionalLogs, filter, dispatch, getState, {
logs: [...totalData.logs, ...additionalLogs.logs], logs: [...totalData.logs, ...additionalLogs.logs],
oldest: additionalLogs.oldest, oldest: additionalLogs.oldest,
}); });
@ -71,31 +76,25 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => {
return totalData; return totalData;
}; };
export const setLogsPagination = createAction('LOGS_PAGINATION');
export const setLogsPage = createAction('SET_LOG_PAGE');
export const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS'); export const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS');
export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsRequest = createAction('GET_LOGS_REQUEST');
export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsFailure = createAction('GET_LOGS_FAILURE');
export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const getLogs = (config) => async (dispatch, getState) => { export const getLogs = () => async (dispatch, getState) => {
dispatch(getLogsRequest()); dispatch(getLogsRequest());
try { try {
const { isFiltered, filter, page } = getState().queryLogs; const { isFiltered, filter, oldest } = getState().queryLogs;
const data = await getLogsWithParams({ const data = await getLogsWithParams({
...config, older_than: oldest,
filter, filter,
}); });
if (isFiltered) { if (isFiltered) {
const additionalData = await checkFilteredLogs(data, filter, dispatch); const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data; const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
dispatch(getLogsSuccess(updatedData)); dispatch(getLogsSuccess(updatedData));
dispatch(setLogsPagination({
page,
pageSize: TABLE_DEFAULT_PAGE_SIZE,
}));
} else { } else {
dispatch(getLogsSuccess(data)); dispatch(getLogsSuccess(data));
} }
@ -111,7 +110,7 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
* *
* @param filter * @param filter
* @param {string} filter.search * @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 * @returns function
*/ */
export const setLogsFilter = (filter) => setLogsFilterRequest(filter); 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 setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS'); export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
export const setFilteredLogs = (filter) => async (dispatch) => { export const setFilteredLogs = (filter) => async (dispatch, getState) => {
dispatch(setFilteredLogsRequest()); dispatch(setFilteredLogsRequest());
try { try {
const data = await getLogsWithParams({ const data = await getLogsWithParams({
older_than: '', older_than: '',
filter, filter,
}); });
const additionalData = await checkFilteredLogs(data, filter, dispatch); const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data; const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
dispatch(setFilteredLogsSuccess({ dispatch(setFilteredLogsSuccess({
...updatedData, ...updatedData,
filter, filter,
})); }));
dispatch(setLogsPage(TABLE_FIRST_PAGE));
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(setFilteredLogsFailure(error)); dispatch(setFilteredLogsFailure(error));

View File

@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { getPathWithQueryString } from '../helpers/helpers'; 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'; import { BASE_URL } from '../../constants';
class Api { class Api {
@ -530,6 +530,8 @@ class Api {
getQueryLog(params) { getQueryLog(params) {
const { path, method } = this.GET_QUERY_LOG; 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); const url = getPathWithQueryString(path, params);
return this.makeRequest(url, method); return this.makeRequest(url, method);
} }

View File

@ -26,7 +26,6 @@ import Header from '../Header';
import { changeLanguage, getDnsStatus } from '../../actions'; import { changeLanguage, getDnsStatus } from '../../actions';
import Dashboard from '../../containers/Dashboard'; import Dashboard from '../../containers/Dashboard';
import Logs from '../../containers/Logs';
import SetupGuide from '../../containers/SetupGuide'; import SetupGuide from '../../containers/SetupGuide';
import Settings from '../../containers/Settings'; import Settings from '../../containers/Settings';
import Dns from '../../containers/Dns'; import Dns from '../../containers/Dns';
@ -38,6 +37,7 @@ import DnsAllowlist from '../../containers/DnsAllowlist';
import DnsRewrites from '../../containers/DnsRewrites'; import DnsRewrites from '../../containers/DnsRewrites';
import CustomRules from '../../containers/CustomRules'; import CustomRules from '../../containers/CustomRules';
import Services from '../Filters/Services'; import Services from '../Filters/Services';
import Logs from '../Logs';
const ROUTES = [ const ROUTES = [

View File

@ -1,14 +1,17 @@
import React from 'react'; import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import PropTypes from 'prop-types'; 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 Card from '../ui/Card';
import Cell from '../ui/Cell'; import Cell from '../ui/Cell';
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers'; import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
import { IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants'; import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
import { formatClientCell } from '../../helpers/formatClientCell'; import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
const getClientsPercentColor = (percent) => { const getClientsPercentColor = (percent) => {
if (percent > 50) { if (percent > 50) {
@ -20,126 +23,131 @@ const getClientsPercentColor = (percent) => {
return STATUS_COLORS.red; return STATUS_COLORS.red;
}; };
const countCell = (dnsQueries) => function cell(row) { const CountCell = (row) => {
const { value } = row; const { value, original: { ip } } = row;
const percent = getPercent(dnsQueries, value); const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual);
const percent = getPercent(numDnsQueries, value);
const percentColor = getClientsPercentColor(percent); 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 renderBlockingButton = (ip) => {
const buttonProps = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND const dispatch = useDispatch();
? { const { t } = useTranslation();
className: 'btn-outline-danger', const processingSet = useSelector((state) => state.access.processingSet);
text: 'block', const disallowed_clients = useSelector(
type: 'block', (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 ( const onClick = () => toggleClientStatus(type, ip);
<div className="table__action button__action">
<button return <div className="table__action pl-4">
type="button" <button
className={`btn btn-sm ${buttonProps.className}`} type="button"
onClick={() => handleClick(buttonProps.type, ip)} className={className}
disabled={processing} onClick={onClick}
> disabled={processingSet}
<Trans>{buttonProps.text}</Trans> >
</button> <Trans>{text}</Trans>
</div> </button>
); </div>;
}; };
const clientCell = (t, toggleClientStatus, processing, disallowedClients) => function cell(row) { const ClientCell = (row) => {
const { value } = row; const { value, original: { info } } = row;
const ipMatchListStatus = getIpMatchListStatus(value, disallowedClients);
return ( return <>
<> <div className="logs__row logs__row--overflow logs__row--column d-flex">
<div className="logs__row logs__row--overflow logs__row--column"> {renderFormattedClientCell(value, info, true)}
{formatClientCell(row, true, false)} {renderBlockingButton(value)}
</div> </div>
{ipMatchListStatus !== IP_MATCH_LIST_STATUS.CIDR </>;
&& renderBlockingButton(ipMatchListStatus, value, toggleClientStatus, processing)}
</>
);
}; };
const Clients = ({ const Clients = ({
t,
refreshButton, refreshButton,
topClients,
subtitle, subtitle,
dnsQueries, }) => {
toggleClientStatus, const { t } = useTranslation();
processingAccessSet, const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
disallowedClients, const disallowedClients = useSelector((state) => state.access.disallowed_clients, shallowEqual);
}) => (
<Card return <Card
title={t('top_clients')} title={t('top_clients')}
subtitle={subtitle} subtitle={subtitle}
bodyType="card-table" bodyType="card-table"
refresh={refreshButton} refresh={refreshButton}
> >
<ReactTable <ReactTable
data={topClients.map(({ data={topClients.map(({
name: ip, count, info, blocked, name: ip, count, info, blocked,
}) => ({ }) => ({
ip, ip,
count, count,
info, info,
blocked, blocked,
}))} }))}
columns={[ columns={[
{ {
Header: 'IP', Header: 'IP',
accessor: 'ip', accessor: 'ip',
sortMethod: sortIp, sortMethod: sortIp,
Cell: clientCell(t, toggleClientStatus, processingAccessSet, disallowedClients), Cell: ClientCell,
}, },
{ {
Header: <Trans>requests_count</Trans>, Header: <Trans>requests_count</Trans>,
accessor: 'count', accessor: 'count',
minWidth: 180, minWidth: 180,
maxWidth: 200, maxWidth: 200,
Cell: countCell(dnsQueries), Cell: CountCell,
}, },
]} ]}
showPagination={false} showPagination={false}
noDataText={t('no_clients_found')} noDataText={t('no_clients_found')}
minRows={6} minRows={6}
defaultPageSize={100} defaultPageSize={100}
className="-highlight card-table-overflow--limited clients__table" className="-highlight card-table-overflow--limited clients__table"
getTrProps={(_state, rowInfo) => { getTrProps={(_state, rowInfo) => {
if (!rowInfo) { if (!rowInfo) {
return {}; return {};
} }
const { ip } = rowInfo.original; const { ip } = rowInfo.original;
return getIpMatchListStatus(ip, disallowedClients) return getIpMatchListStatus(ip, disallowedClients) === IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' };
=== IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' }; }}
}}
/> />
</Card> </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,
}; };
export default withTranslation()(Clients); Clients.propTypes = {
refreshButton: PropTypes.node.isRequired,
subtitle: PropTypes.string.isRequired,
};
export default Clients;

View File

@ -47,32 +47,32 @@ const Counters = ({ refreshButton, subtitle }) => {
label: 'dns_query', label: 'dns_query',
count: numDnsQueries, count: numDnsQueries,
tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }), 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', label: 'blocked_by',
count: numBlockedFiltering, count: numBlockedFiltering,
tooltipTitle: 'number_of_dns_query_blocked_24_hours', 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>], translationComponents: [<a href="#filters" key="0">link</a>],
}, },
{ {
label: 'stats_malware_phishing', label: 'stats_malware_phishing',
count: numReplacedSafebrowsing, count: numReplacedSafebrowsing,
tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec', 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', label: 'stats_adult',
count: numReplacedParental, count: numReplacedParental,
tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult', 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', label: 'enforced_save_search',
count: numReplacedSafesearch, count: numReplacedSafesearch,
tooltipTitle: 'number_of_dns_query_to_safe_search', 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', label: 'average_processing_time',

View File

@ -10,7 +10,6 @@ import BlockedDomains from './BlockedDomains';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import { BLOCK_ACTIONS } from '../../helpers/constants';
import './Dashboard.css'; import './Dashboard.css';
const Dashboard = ({ const Dashboard = ({
@ -19,7 +18,6 @@ const Dashboard = ({
getStatsConfig, getStatsConfig,
dashboard, dashboard,
toggleProtection, toggleProtection,
toggleClientBlock,
stats, stats,
access, access,
}) => { }) => {
@ -50,14 +48,6 @@ const Dashboard = ({
</button>; </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 const refreshButton = <button
type="button" type="button"
className="btn btn-icon btn-outline-primary btn-sm" className="btn btn-icon btn-outline-primary btn-sm"
@ -122,7 +112,6 @@ const Dashboard = ({
clients={dashboard.clients} clients={dashboard.clients}
autoClients={dashboard.autoClients} autoClients={dashboard.autoClients}
refreshButton={refreshButton} refreshButton={refreshButton}
toggleClientStatus={toggleClientStatus}
processingAccessSet={access.processingSet} processingAccessSet={access.processingSet}
disallowedClients={access.disallowed_clients} disallowedClients={access.disallowed_clients}
/> />
@ -157,7 +146,6 @@ Dashboard.propTypes = {
getStatsConfig: PropTypes.func.isRequired, getStatsConfig: PropTypes.func.isRequired,
toggleProtection: PropTypes.func.isRequired, toggleProtection: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired, getClients: PropTypes.func.isRequired,
toggleClientBlock: PropTypes.func.isRequired,
getAccessList: PropTypes.func.isRequired, getAccessList: PropTypes.func.isRequired,
}; };

View File

@ -11,25 +11,10 @@ import {
checkWhiteList, checkWhiteList,
checkSafeSearch, checkSafeSearch,
checkSafeBrowsing, checkSafeBrowsing,
checkParental, checkParental, getFilterName,
} from '../../../helpers/helpers'; } from '../../../helpers/helpers';
import { FILTERED } from '../../../helpers/constants'; 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) => { const getTitle = (reason, filterName, t, onlyFiltered) => {
if (checkNotFilteredNotFound(reason)) { if (checkNotFilteredNotFound(reason)) {
return t('check_not_found'); return t('check_not_found');
@ -101,7 +86,12 @@ const Info = ({
ip_addrs, ip_addrs,
t, 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) const onlyFiltered = checkSafeSearch(reason)
|| checkSafeBrowsing(reason) || checkSafeBrowsing(reason)
|| checkParental(reason); || checkParental(reason);

View File

@ -48,7 +48,7 @@ class Table extends Component {
accessor: 'url', accessor: 'url',
minWidth: 200, minWidth: 200,
Cell: ({ value }) => ( Cell: ({ value }) => (
<div className="logs__row o-hidden"> <div className="logs__row">
{isValidAbsolutePath(value) ? value {isValidAbsolutePath(value) ? value
: <a : <a
href={value} href={value}

View 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;

View 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;

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import propTypes from 'prop-types';
import getIconTooltip from './getIconTooltip'; import { useTranslation } from 'react-i18next';
import { import {
DEFAULT_SHORT_DATE_FORMAT_OPTIONS, DEFAULT_SHORT_DATE_FORMAT_OPTIONS,
LONG_TIME_FORMAT, LONG_TIME_FORMAT,
@ -9,15 +10,19 @@ import {
} from '../../../helpers/constants'; } from '../../../helpers/constants';
import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers'; import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers';
import { getSourceData } from '../../../helpers/trackers/trackers'; import { getSourceData } from '../../../helpers/trackers/trackers';
import IconTooltip from './IconTooltip';
const getDomainCell = (props) => { const DomainCell = ({
const { answer_dnssec,
row, t, isDetailed, dnssec_enabled, client_proto,
} = props; domain,
time,
const { tracker,
tracker, type, answer_dnssec, client_proto, domain, time, type,
} = row.original; }) => {
const { t } = useTranslation();
const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled);
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const hasTracker = !!tracker; const hasTracker = !!tracker;
@ -50,8 +55,8 @@ const getDomainCell = (props) => {
name_table_header: tracker?.name, name_table_header: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
source_label: sourceData source_label: sourceData
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer" && <a href={sourceData.url} target="_blank" rel="noopener noreferrer"
className="link--green">{sourceData.name}</a>, className="link--green">{sourceData.name}</a>,
}; };
const renderGrid = (content, idx) => { const renderGrid = (content, idx) => {
@ -72,51 +77,42 @@ const getDomainCell = (props) => {
const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails; const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails;
const trackerHint = getIconTooltip({ const valueClass = classNames('w-100 text-truncate', {
className: privacyIconClass,
tooltipClass: 'pt-4 pb-5 px-5 mw-75',
xlinkHref: 'privacy',
contentItemClass: 'key-colon',
renderContent,
place: 'bottom',
});
const valueClass = classNames('w-100', {
'px-2 d-flex justify-content-center flex-column': isDetailed, 'px-2 d-flex justify-content-center flex-column': isDetailed,
}); });
const details = [ip, protocol].filter(Boolean) const details = [ip, protocol].filter(Boolean)
.join(', '); .join(', ');
return ( return <div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell">
<div className="logs__row o-hidden"> {dnssec_enabled && <IconTooltip
{dnssec_enabled && getIconTooltip({ className={lockIconClass}
className: lockIconClass, tooltipClass='py-4 px-5 pb-45'
tooltipClass: 'py-4 px-5 pb-45', canShowTooltip={!!answer_dnssec}
canShowTooltip: answer_dnssec, xlinkHref='lock'
xlinkHref: 'lock', columnClass='w-100'
columnClass: 'w-100', content='validated_with_dnssec'
content: 'validated_with_dnssec', placement='bottom'
placement: 'bottom', />}
})} <IconTooltip className={privacyIconClass} tooltipClass='pt-4 pb-5 px-5 mw-75'
{trackerHint} xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent}
<div className={valueClass}> place='bottom' />
<div className="text-truncate" title={domain}>{domain}</div> <div className={valueClass}>
{details && isDetailed <div className="text-truncate" title={domain}>{domain}</div>
&& <div className="detailed-info d-none d-sm-block text-truncate" {details && isDetailed
title={details}>{details}</div>} && <div className="detailed-info d-none d-sm-block text-truncate"
</div> title={details}>{details}</div>}
</div> </div>
); </div>;
}; };
getDomainCell.propTypes = { DomainCell.propTypes = {
row: PropTypes.object.isRequired, answer_dnssec: propTypes.bool.isRequired,
t: PropTypes.func.isRequired, client_proto: propTypes.string.isRequired,
isDetailed: PropTypes.bool.isRequired, domain: propTypes.string.isRequired,
toggleBlocking: PropTypes.func.isRequired, time: propTypes.string.isRequired,
autoClients: PropTypes.array.isRequired, type: propTypes.string.isRequired,
dnssec_enabled: PropTypes.bool.isRequired, tracker: propTypes.object,
}; };
export default getDomainCell; export default DomainCell;

View 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;

View 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;

View File

@ -7,7 +7,7 @@ import Tooltip from '../../ui/Tooltip';
import 'react-popper-tooltip/dist/styles.css'; import 'react-popper-tooltip/dist/styles.css';
import './IconTooltip.css'; import './IconTooltip.css';
const getIconTooltip = ({ const IconTooltip = ({
className, className,
contentItemClass, contentItemClass,
columnClass, columnClass,
@ -43,14 +43,14 @@ const getIconTooltip = ({
</Tooltip>; </Tooltip>;
}; };
getIconTooltip.propTypes = { IconTooltip.propTypes = {
className: PropTypes.string, className: PropTypes.string,
contentItemClass: PropTypes.string, contentItemClass: PropTypes.string,
columnClass: PropTypes.string, columnClass: PropTypes.string,
tooltipClass: PropTypes.string, tooltipClass: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
placement: PropTypes.string, placement: PropTypes.string,
canShowTooltip: PropTypes.string, canShowTooltip: PropTypes.bool,
xlinkHref: PropTypes.string, xlinkHref: PropTypes.string,
content: PropTypes.oneOfType([ content: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
@ -59,4 +59,4 @@ getIconTooltip.propTypes = {
renderContent: PropTypes.arrayOf(PropTypes.element), renderContent: PropTypes.arrayOf(PropTypes.element),
}; };
export default getIconTooltip; export default IconTooltip;

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -107,7 +107,7 @@ const Form = (props) => {
const { const {
response_status, search, response_status, search,
} = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual); } = useSelector((state) => state?.form[FORM_NAME.LOGS_FILTER].values, shallowEqual);
const [ const [
debouncedSearch, debouncedSearch,
@ -171,14 +171,14 @@ const Form = (props) => {
> >
{Object.values(RESPONSE_FILTER) {Object.values(RESPONSE_FILTER)
.map(({ .map(({
query, label, disabled, QUERY, LABEL, disabled,
}) => ( }) => (
<option <option
key={label} key={LABEL}
value={query} value={QUERY}
disabled={disabled} disabled={disabled}
> >
{t(label)} {t(LABEL)}
</option> </option>
)) ))
} }
@ -197,5 +197,4 @@ Form.propTypes = {
export default reduxForm({ export default reduxForm({
form: FORM_NAME.LOGS_FILTER, form: FORM_NAME.LOGS_FILTER,
enableReinitialize: true,
})(Form); })(Form);

View File

@ -1,10 +1,21 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import Form from './Form'; 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 { 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"> return <div className="page-header page-header--logs">
<h1 className="page-title page-title--large"> <h1 className="page-title page-title--large">
@ -29,7 +40,6 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => {
Filters.propTypes = { Filters.propTypes = {
filter: PropTypes.object.isRequired, filter: PropTypes.object.isRequired,
refreshLogs: PropTypes.func.isRequired,
processingGetLogs: PropTypes.bool.isRequired, processingGetLogs: PropTypes.bool.isRequired,
setIsLoading: PropTypes.func.isRequired, setIsLoading: PropTypes.func.isRequired,
}; };

View 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;

View File

@ -1,44 +1,21 @@
:root { :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-4d: #4D4D4D;
--gray-8: #888; --gray-8: #888;
--danger: #DF3812; --danger: #DF3812;
--white80: rgba(255, 255, 255, 0.8);
} }
.logs__row { .logs__text {
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 {
padding: 0 1px; padding: 0 1px;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -54,237 +31,6 @@
font-weight: bold; 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 { .logs__time {
font-size: 1rem; font-size: 1rem;
line-height: 1.5; line-height: 1.5;
@ -302,132 +48,24 @@
border-radius: 4px; 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 { .text-pre {
white-space: pre-wrap !important; white-space: pre-wrap !important;
overflow-wrap: break-word; overflow-wrap: break-word;
overflow: visible; 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 { .link--green {
color: var(--green79); color: var(--green79);
} }
.row--detailed {
height: 4.9rem
}
.w-90 { .w-90 {
max-width: 90% !important; max-width: 90% !important;
} }
.h-85 {
height: 85% !important;
}
.pt-45 {
padding-top: 1.25rem !important;
}
.pb-45 { .pb-45 {
padding-bottom: 1.25rem !important; padding-bottom: 1.25rem !important;
} }
.py-45 {
padding-top: 1.25rem !important;
padding-bottom: 1.25rem !important;
}
.mh-100 { .mh-100 {
max-height: 100% !important; max-height: 100% !important;
} }
@ -493,14 +131,6 @@
} }
@media (max-width: 767.98px) { @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 { .form-control--container {
width: 100%; width: 100%;
flex-direction: column; flex-direction: column;
@ -517,38 +147,157 @@
} }
} }
@media (max-width: 575px) { @media screen and (max-width: 767.98px) {
.logs__table .rt-tr { .logs__table .logs__cell--response,
height: 3.125rem; .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 { .logs__refresh {
--size: 2.5rem;
position: relative; position: relative;
top: 3px; top: 3px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 40px; width: var(--size);
height: 40px; height: var(--size);
padding: 0; padding: 0;
margin-left: 15px; margin-left: 0.9375rem;
background-color: transparent; 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%;
}

View File

@ -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;

View File

@ -1,5 +1,4 @@
import React, { Fragment, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import Modal from 'react-modal'; import Modal from 'react-modal';
import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
@ -8,24 +7,21 @@ import queryString from 'query-string';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
BLOCK_ACTIONS, BLOCK_ACTIONS,
TABLE_DEFAULT_PAGE_SIZE,
TABLE_FIRST_PAGE,
SMALL_SCREEN_SIZE, SMALL_SCREEN_SIZE,
} from '../../helpers/constants'; } from '../../helpers/constants';
import Loading from '../ui/Loading'; import Loading from '../ui/Loading';
import Filters from './Filters'; import Filters from './Filters';
import Table from './Table';
import Disabled from './Disabled'; import Disabled from './Disabled';
import { getFilteringStatus } from '../../actions/filtering'; import { getFilteringStatus } from '../../actions/filtering';
import { getClients } from '../../actions'; import { getClients } from '../../actions';
import { getDnsConfig } from '../../actions/dnsConfig'; import { getDnsConfig } from '../../actions/dnsConfig';
import { import {
getLogsConfig, getLogsConfig,
refreshFilteredLogs,
resetFilteredLogs, resetFilteredLogs,
setFilteredLogs, setFilteredLogs,
toggleDetailedLogs,
} from '../../actions/queryLogs'; } from '../../actions/queryLogs';
import { addSuccessToast } from '../../actions/toasts'; import InfiniteTable from './InfiniteTable';
import './Logs.css'; import './Logs.css';
const processContent = (data, buttonType) => Object.entries(data) const processContent = (data, buttonType) => Object.entries(data)
@ -48,21 +44,20 @@ const processContent = (data, buttonType) => Object.entries(data)
keyClass = ''; keyClass = '';
} }
return isHidden ? null : <Fragment key={key}> return isHidden ? null : <div key={key}>
<div <div
className={classNames(`key__${key}`, keyClass, { className={classNames(`key__${key}`, keyClass, {
'font-weight-bold': isBoolean && value === true, 'font-weight-bold': isBoolean && value === true,
})}> })}>
<Trans>{isButton ? value : key}</Trans> <Trans>{isButton ? value : key}</Trans>
</div> </div>
<div className={`value__${key} text-pre text-truncate`}> <div className={`value__${key} text-pre text-truncate`}>
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans> <Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
</div> </div>
</Fragment>; </div>;
}); });
const Logs = () => {
const Logs = (props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
@ -71,7 +66,14 @@ const Logs = (props) => {
search: search_url_param = '', search: search_url_param = '',
} = queryString.parse(history.location.search); } = 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 search = filter?.search || search_url_param;
const response_status = filter?.response_status || response_status_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 [isModalOpened, setModalOpened] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const closeModal = () => setModalOpened(false);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -94,44 +97,11 @@ const Logs = (props) => {
})(); })();
}, [response_status, search]); }, [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 mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`);
const mediaQueryHandler = (e) => { const mediaQueryHandler = (e) => {
setIsSmallScreen(e.matches); setIsSmallScreen(e.matches);
if (e.matches) { if (e.matches) {
toggleDetailedLogs(false); dispatch(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,
});
} }
}; };
@ -149,7 +119,6 @@ const Logs = (props) => {
(async () => { (async () => {
setIsLoading(true); setIsLoading(true);
dispatch(setLogsPage(TABLE_FIRST_PAGE));
dispatch(getFilteringStatus()); dispatch(getFilteringStatus());
dispatch(getClients()); dispatch(getClients());
try { try {
@ -169,6 +138,7 @@ const Logs = (props) => {
mediaQuery.removeEventListener('change', mediaQueryHandler); mediaQuery.removeEventListener('change', mediaQueryHandler);
} catch (e1) { } catch (e1) {
try { try {
// Safari 13.1 do not support mediaQuery.addEventListener('change', handler)
mediaQuery.removeListener(mediaQueryHandler); mediaQuery.removeListener(mediaQueryHandler);
} catch (e2) { } catch (e2) {
console.error(e2); console.error(e2);
@ -179,99 +149,53 @@ const Logs = (props) => {
}; };
}, []); }, []);
const refreshLogs = async () => { const renderPage = () => <>
setIsLoading(true); <Filters
await Promise.all([ filter={{
dispatch(setLogsPage(TABLE_FIRST_PAGE)), response_status,
dispatch(refreshFilteredLogs()), search,
]); }}
dispatch(addSuccessToast('query_log_updated')); setIsLoading={setIsLoading}
setIsLoading(false); 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 ( return <>
<> {enabled && processingGetConfig && <Loading />}
{enabled && processingGetConfig && <Loading />} {enabled && !processingGetConfig && renderPage()}
{enabled && !processingGetConfig && ( {!enabled && !processingGetConfig && <Disabled />}
<> </>;
<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,
}; };
export default Logs; export default Logs;

View File

@ -77,12 +77,12 @@ const StaticLeases = ({
title={t('delete_table_action')} title={t('delete_table_action')}
disabled={processingDeleting} disabled={processingDeleting}
onClick={() => handleDelete(ip, mac, hostname)} onClick={() => handleDelete(ip, mac, hostname)}
> >
<svg className="icons"> <svg className="icons">
<use xlinkHref="#delete" /> <use xlinkHref="#delete"/>
</svg> </svg>
</button> </button>
</div>; </div>;
}, },
}, },
]} ]}

View File

@ -1,71 +1,50 @@
import React, { Component } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FAILURE_TOAST_TIMEOUT, SUCCESS_TOAST_TIMEOUT } from '../../helpers/constants'; import { useDispatch } from 'react-redux';
import { TOAST_TIMEOUTS } from '../../helpers/constants';
import { removeToast } from '../../actions';
class Toast extends Component { const Toast = ({
state = { id,
timerId: null, 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() { useEffect(() => {
this.setRemoveToastTimeout(); setRemoveToastTimeout();
} }, []);
shouldComponentUpdate() { return <div className={`toast toast--${type}`}
return false; onMouseOver={clearRemoveToastTimeout}
} onMouseOut={setRemoveToastTimeout}>
<p className="toast__content">{t(message)}</p>
clearRemoveToastTimeout = () => clearTimeout(this.state.timerId); <button className="toast__dismiss" onClick={removeCurrentToast}>
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
setRemoveToastTimeout = () => { viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
const timeout = this.props.type === 'success' ? SUCCESS_TOAST_TIMEOUT : FAILURE_TOAST_TIMEOUT; <path d="m18 6-12 12" />
<path d="m6 6 12 12" />
const timerId = setTimeout(() => { </svg>
this.props.removeToast(this.props.id); </button>
}, timeout); </div>;
};
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>
);
}
}
Toast.propTypes = { Toast.propTypes = {
t: PropTypes.func.isRequired,
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired, message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
removeToast: PropTypes.func.isRequired,
}; };
export default withTranslation()(Toast); export default Toast;

View File

@ -1,41 +1,25 @@
import { connect } from 'react-redux';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { useSelector, shallowEqual } from 'react-redux';
import { CSSTransition, TransitionGroup } from 'react-transition-group'; 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 from './Toast';
import './Toast.css'; import './Toast.css';
const Toasts = (props) => ( const Toasts = () => {
<TransitionGroup className="toasts"> const toasts = useSelector((state) => state.toasts, shallowEqual);
{props.toasts.notices?.map((toast) => {
const { id } = toast;
return (
<CSSTransition
key={id}
timeout={500}
classNames="toast"
>
<Toast removeToast={props.removeToast} {...toast} />
</CSSTransition>
);
})}
</TransitionGroup>
);
Toasts.propTypes = { return <TransitionGroup className="toasts">
toasts: PropTypes.object, {toasts.notices?.map((toast) => {
removeToast: PropTypes.func, const { id } = toast;
return <CSSTransition
key={id}
timeout={TOAST_TRANSITION_TIMEOUT}
classNames="toast"
>
<Toast {...toast} />
</CSSTransition>;
})}
</TransitionGroup>;
}; };
const mapStateToProps = (state) => { export default Toasts;
const { toasts } = state;
const props = { toasts };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Toasts);

View File

@ -118,14 +118,14 @@
} }
} }
.card .red { .card .logs__cell--red {
background-color: #fff4f2; background-color: #fff4f2;
} }
.card .green { .card .logs__cell--green {
background-color: #f1faf3; background-color: #f1faf3;
} }
.card .blue { .card .logs__row--blue {
background-color: #ecf7ff; background-color: #ecf7ff;
} }

View File

@ -13,8 +13,7 @@
z-index: 100; z-index: 100;
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
background-color: rgba(255, 255, 255, 0.6); background-color: rgba(255, 255, 255, 0.48);
opacity: 0.8;
} }
.loading:after { .loading:after {

View File

@ -1,14 +1,17 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import './Loading.css'; import './Loading.css';
const Loading = ({ className }) => ( const Loading = ({ className, text }) => {
<div className={classNames('loading', className)} /> const { t } = useTranslation();
); return <div className={classNames('loading', className)}>{t(text)}</div>;
};
Loading.propTypes = { Loading.propTypes = {
className: PropTypes.string, className: PropTypes.string,
text: PropTypes.string,
}; };
export default Loading; export default Loading;

View File

@ -13,18 +13,18 @@
overflow: visible; overflow: visible;
} }
.rt-tr-group.red { .rt-tr-group.logs__row--red {
background-color: rgba(223, 56, 18, 0.05); 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); background-color: rgba(103, 178, 121, 0.1);
} }
.rt-tr-group.blue { .rt-tr-group.logs__row--blue {
background-color: #e5effd; background-color: #e5effd;
} }
.rt-tr-group.yellow { .rt-tr-group.logs__row--yellow {
background-color: var(--yellow-pale); background-color: var(--yellow-pale);
} }

View File

@ -34,33 +34,47 @@ const Tooltip = ({
delayShowValue = 0; 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 ( return (
<TooltipTrigger <TooltipTrigger
placement={placement} placement={placement}
trigger={triggerValue} trigger={triggerValue}
delayHide={delayHideValue} delayHide={delayHideValue}
delayShow={delayShowValue} delayShow={delayShowValue}
tooltip={({ tooltipRef, getTooltipProps }) => ( tooltip={renderTooltip}
<div
{...getTooltipProps({
ref: tooltipRef,
className,
})}
>
{typeof content === 'string' ? t(content) : content}
</div>
)}
> >
{({ getTriggerProps, triggerRef }) => ( {renderTrigger}
<span
{...getTriggerProps({
ref: triggerRef,
className: triggerClass,
})}
>
{children}
</span>
)}
</TooltipTrigger> </TooltipTrigger>
); );
}; };

View File

@ -1,7 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { toggleProtection, getClients } from '../actions'; import { toggleProtection, getClients } from '../actions';
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats'; import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
import { toggleClientBlock, getAccessList } from '../actions/access'; import { getAccessList } from '../actions/access';
import Dashboard from '../components/Dashboard'; import Dashboard from '../components/Dashboard';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
@ -16,7 +16,6 @@ const mapDispatchToProps = {
getStats, getStats,
getStatsConfig, getStatsConfig,
setStatsConfig, setStatsConfig,
toggleClientBlock,
getAccessList, getAccessList,
}; };

View File

@ -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);

View File

@ -307,9 +307,7 @@ export const DEFAULT_LOGS_FILTER = {
export const DEFAULT_LANGUAGE = 'en'; export const DEFAULT_LANGUAGE = 'en';
export const TABLE_DEFAULT_PAGE_SIZE = 25; export const QUERY_LOGS_PAGE_LIMIT = 20;
export const TABLE_FIRST_PAGE = 0;
export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20; export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20;
@ -327,85 +325,93 @@ export const FILTERED_STATUS = {
export const RESPONSE_FILTER = { export const RESPONSE_FILTER = {
ALL: { ALL: {
query: 'all', QUERY: 'all',
label: 'all_queries', LABEL: 'all_queries',
}, },
FILTERED: { FILTERED: {
query: 'filtered', QUERY: 'filtered',
label: 'filtered', LABEL: 'filtered',
}, },
PROCESSED: { PROCESSED: {
query: 'processed', QUERY: 'processed',
label: 'show_processed_responses', LABEL: 'show_processed_responses',
}, },
BLOCKED: { BLOCKED: {
query: 'blocked', QUERY: 'blocked',
label: 'show_blocked_responses', LABEL: 'show_blocked_responses',
}, },
BLOCKED_THREATS: { BLOCKED_THREATS: {
query: 'blocked_safebrowsing', QUERY: 'blocked_safebrowsing',
label: 'blocked_threats', LABEL: 'blocked_threats',
}, },
BLOCKED_ADULT_WEBSITES: { BLOCKED_ADULT_WEBSITES: {
query: 'blocked_parental', QUERY: 'blocked_parental',
label: 'blocked_adult_websites', LABEL: 'blocked_adult_websites',
}, },
ALLOWED: { ALLOWED: {
query: 'whitelisted', QUERY: 'whitelisted',
label: 'allowed', LABEL: 'allowed',
}, },
REWRITTEN: { REWRITTEN: {
query: 'rewritten', QUERY: 'rewritten',
label: 'rewritten', LABEL: 'rewritten',
}, },
SAFE_SEARCH: { SAFE_SEARCH: {
query: 'safe_search', QUERY: 'safe_search',
label: 'safe_search', LABEL: 'safe_search',
}, },
}; };
export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER) export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER)
.reduce((acc, { query }) => { .reduce((acc, { QUERY }) => {
acc[query] = query; acc[QUERY] = QUERY;
return acc; return acc;
}, {}); }, {});
export const QUERY_STATUS_COLORS = {
BLUE: 'blue',
GREEN: 'green',
RED: 'red',
WHITE: 'white',
YELLOW: 'yellow',
};
export const FILTERED_STATUS_TO_META_MAP = { export const FILTERED_STATUS_TO_META_MAP = {
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: { [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: {
label: RESPONSE_FILTER.ALLOWED.label, LABEL: RESPONSE_FILTER.ALLOWED.LABEL,
color: 'green', COLOR: QUERY_STATUS_COLORS.GREEN,
}, },
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: { [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: {
label: RESPONSE_FILTER.PROCESSED.label, LABEL: RESPONSE_FILTER.PROCESSED.LABEL,
color: 'white', COLOR: QUERY_STATUS_COLORS.WHITE,
}, },
[FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: { [FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: {
label: RESPONSE_FILTER.BLOCKED.label, LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
color: 'red', COLOR: QUERY_STATUS_COLORS.RED,
}, },
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: { [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: {
label: RESPONSE_FILTER.SAFE_SEARCH.label, LABEL: RESPONSE_FILTER.SAFE_SEARCH.LABEL,
color: 'yellow', COLOR: QUERY_STATUS_COLORS.YELLOW,
}, },
[FILTERED_STATUS.FILTERED_BLACK_LIST]: { [FILTERED_STATUS.FILTERED_BLACK_LIST]: {
label: RESPONSE_FILTER.BLOCKED.label, LABEL: RESPONSE_FILTER.BLOCKED.LABEL,
color: 'red', COLOR: QUERY_STATUS_COLORS.RED,
}, },
[FILTERED_STATUS.REWRITE]: { [FILTERED_STATUS.REWRITE]: {
label: RESPONSE_FILTER.REWRITTEN.label, LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
color: 'blue', COLOR: QUERY_STATUS_COLORS.BLUE,
}, },
[FILTERED_STATUS.REWRITE_HOSTS]: { [FILTERED_STATUS.REWRITE_HOSTS]: {
label: RESPONSE_FILTER.REWRITTEN.label, LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
color: 'blue', COLOR: QUERY_STATUS_COLORS.BLUE,
}, },
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: { [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: {
label: RESPONSE_FILTER.BLOCKED_THREATS.label, LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL,
color: 'yellow', COLOR: QUERY_STATUS_COLORS.YELLOW,
}, },
[FILTERED_STATUS.FILTERED_PARENTAL]: { [FILTERED_STATUS.FILTERED_PARENTAL]: {
label: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.label, LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL,
color: 'yellow', COLOR: QUERY_STATUS_COLORS.YELLOW,
}, },
}; };
@ -519,3 +525,17 @@ export const DHCP_DESCRIPTION_PLACEHOLDERS = {
lease_duration: 'dhcp_form_lease_input', 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,
};

View File

@ -15,6 +15,7 @@ import { getTrackerData } from './trackers/trackers';
import { import {
CHECK_TIMEOUT, CHECK_TIMEOUT,
CUSTOM_FILTERING_RULES_ID,
DEFAULT_DATE_FORMAT_OPTIONS, DEFAULT_DATE_FORMAT_OPTIONS,
DEFAULT_LANGUAGE, DEFAULT_LANGUAGE,
DEFAULT_TIME_FORMAT, 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 ip {string}
* @param gateway_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); acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses);
return acc; return acc;
}, interfaces); }, interfaces);
export const isScrolledIntoView = (el) => {
const rect = el.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
return elemTop < window.innerHeight && elemBottom >= 0;
};

View File

@ -24,9 +24,17 @@ const getFormattedWhois = (whois) => {
); );
}; };
export const formatClientCell = (row, isDetailed = false, isLogs = true) => { /**
const { value, original: { info } } = row; * @param {string} value
let whoisContainer = ''; * @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; let nameContainer = value;
if (info) { if (info) {
@ -34,42 +42,28 @@ export const formatClientCell = (row, isDetailed = false, isLogs = true) => {
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0; const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
if (name) { if (name) {
if (isLogs) { const nameValue = <div className="logs__text logs__text--nowrap" title={`${name} (${value})`}>
nameContainer = !whoisAvailable && isDetailed {name}&nbsp;<small>{`(${value})`}</small>
? ( </div>;
<small title={value}>{value}</small>
) : ( if (!isLogs) {
<div className="logs__text logs__text--nowrap" title={`${name} (${value})`}> nameContainer = nameValue;
{name}&nbsp;<small>{`(${value})`}</small>
</div>
);
} else { } else {
nameContainer = ( nameContainer = !whoisAvailable && isDetailed
<div ? <small title={value}>{value}</small>
className="logs__text logs__text--nowrap" : nameValue;
title={`${name} (${value})`}
>
{name}&nbsp;<small>{`(${value})`}</small>
</div>
);
} }
} }
if (whoisAvailable && isDetailed) { if (whoisAvailable && isDetailed) {
whoisContainer = ( whoisContainer = <div className="logs__text logs__text--wrap logs__text--whois">
<div className="logs__text logs__text--wrap logs__text--whois">
{getFormattedWhois(whois_info)} {getFormattedWhois(whois_info)}
</div> </div>;
);
} }
} }
return ( return <div className="logs__text mw-100" title={value}>
<div className="logs__text mw-100" title={value}> {nameContainer}
<> {whoisContainer}
{nameContainer} </div>;
{whoisContainer}
</>
</div>
);
}; };

View File

@ -1,30 +1,10 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import * as actions from '../actions/queryLogs'; 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( 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.setFilteredLogsRequest]: (state) => ({ ...state, processingGetLogs: true }),
[actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }), [actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
[actions.toggleDetailedLogs]: (state, { payload }) => ({ [actions.toggleDetailedLogs]: (state, { payload }) => ({
@ -34,14 +14,7 @@ const queryLogs = handleActions(
[actions.setFilteredLogsSuccess]: (state, { payload }) => { [actions.setFilteredLogsSuccess]: (state, { payload }) => {
const { logs, oldest, filter } = 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]); const isFiltered = filter && Object.keys(filter).some((key) => filter[key]);
return { return {
@ -49,10 +22,8 @@ const queryLogs = handleActions(
oldest, oldest,
filter, filter,
isFiltered, isFiltered,
pages, logs,
total, isEntireLog: logs.length < 1,
logs: logsSlice,
allLogs: logs,
processingGetLogs: false, processingGetLogs: false,
}; };
}, },
@ -67,29 +38,13 @@ const queryLogs = handleActions(
[actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }), [actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }),
[actions.getLogsSuccess]: (state, { payload }) => { [actions.getLogsSuccess]: (state, { payload }) => {
const { const {
logs, oldest, older_than, page, pageSize, initial, logs, oldest, older_than,
} = payload; } = 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 { return {
...state, ...state,
oldest, oldest,
pages, logs: older_than ? [...state.logs, ...logs] : logs,
total,
allLogs,
logs: logsSlice,
isEntireLog: logs.length < 1, isEntireLog: logs.length < 1,
processingGetLogs: false, processingGetLogs: false,
}; };
@ -126,7 +81,7 @@ const queryLogs = handleActions(
...state, processingAdditionalLogs: false, processingGetLogs: false, ...state, processingAdditionalLogs: false, processingGetLogs: false,
}), }),
[actions.getAdditionalLogsSuccess]: (state) => ({ [actions.getAdditionalLogsSuccess]: (state) => ({
...state, processingAdditionalLogs: false, processingGetLogs: false, ...state, processingAdditionalLogs: false, processingGetLogs: false, isEntireLog: true,
}), }),
}, },
{ {
@ -135,18 +90,15 @@ const queryLogs = handleActions(
processingGetConfig: false, processingGetConfig: false,
processingSetConfig: false, processingSetConfig: false,
processingAdditionalLogs: false, processingAdditionalLogs: false,
logs: [],
interval: 1, interval: 1,
allLogs: [], logs: [],
page: 0,
pages: 0,
total: 0,
enabled: true, enabled: true,
oldest: '', oldest: '',
filter: DEFAULT_LOGS_FILTER, filter: DEFAULT_LOGS_FILTER,
isFiltered: false, isFiltered: false,
anonymize_client_ip: false, anonymize_client_ip: false,
isDetailed: true, isDetailed: true,
isEntireLog: false,
}, },
); );

View File

@ -5,16 +5,17 @@ import {
addErrorToast, addNoticeToast, addSuccessToast, addErrorToast, addNoticeToast, addSuccessToast,
} from '../actions/toasts'; } from '../actions/toasts';
import { removeToast } from '../actions'; import { removeToast } from '../actions';
import { TOAST_TYPES } from '../helpers/constants';
const toasts = handleActions({ const toasts = handleActions({
[addErrorToast]: (state, { payload }) => { [addErrorToast]: (state, { payload }) => {
const message = payload.error.toString(); const message = payload.error.toString();
console.error(message); console.error(payload.error);
const errorToast = { const errorToast = {
id: nanoid(), id: nanoid(),
message, message,
type: 'error', type: TOAST_TYPES.ERROR,
}; };
const newState = { ...state, notices: [...state.notices, errorToast] }; const newState = { ...state, notices: [...state.notices, errorToast] };
@ -24,7 +25,7 @@ const toasts = handleActions({
const successToast = { const successToast = {
id: nanoid(), id: nanoid(),
message: payload, message: payload,
type: 'success', type: TOAST_TYPES.SUCCESS,
}; };
const newState = { ...state, notices: [...state.notices, successToast] }; const newState = { ...state, notices: [...state.notices, successToast] };
@ -34,7 +35,7 @@ const toasts = handleActions({
const noticeToast = { const noticeToast = {
id: nanoid(), id: nanoid(),
message: payload.error.toString(), message: payload.error.toString(),
type: 'notice', type: TOAST_TYPES.NOTICE,
}; };
const newState = { ...state, notices: [...state.notices, noticeToast] }; const newState = { ...state, notices: [...state.notices, noticeToast] };

View File

@ -41,9 +41,8 @@ const config = {
alias: { alias: {
MainRoot: path.resolve(__dirname, '../'), MainRoot: path.resolve(__dirname, '../'),
ClientRoot: path.resolve(__dirname, './src'), ClientRoot: path.resolve(__dirname, './src'),
// TODO: change to '@hot-loader/react-dom' when v16.13.1 is released // TODO: uncomment when v16.13.1 is released https://stackoverflow.com/a/62671689/12942752
// https://stackoverflow.com/a/62671689/12942752 // 'react-dom': '@hot-loader/react-dom',
'react-dom': 'react-dom',
}, },
}, },
module: { module: {

View File

@ -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 { if limit, err := strconv.ParseInt(q.Get("limit"), 10, 64); err == nil {
p.limit = int(limit) 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 { if offset, err := strconv.ParseInt(q.Get("offset"), 10, 64); err == nil {
p.offset = int(offset) 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 p.maxFileScanEntries = 0
} }