diff --git a/client/package-lock.json b/client/package-lock.json index 951dac9a..84c8e186 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1356,17 +1356,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, - "@hot-loader/react-dom": { - "version": "16.13.0", - "resolved": "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-16.13.0.tgz", - "integrity": "sha512-lJZrmkucz2MrQJTQtJobx5MICXcfQvKihszqv655p557HPi0hMOWxrNpiHv3DWD8ugNWjtWcVWqRnFvwsHq1mQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.0" - } - }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -10784,6 +10773,24 @@ "yallist": "^4.0.0" } }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -12169,9 +12176,9 @@ } }, "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "requires": { "end-of-stream": "^1.1.0", @@ -12187,6 +12194,18 @@ "duplexify": "^3.6.0", "inherits": "^2.0.3", "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } } }, "punycode": { @@ -13281,10 +13300,13 @@ } }, "serialize-javascript": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", - "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", - "dev": true + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -14619,16 +14641,16 @@ } }, "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -14688,15 +14710,6 @@ "path-exists": "^3.0.0" } }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -14707,24 +14720,6 @@ "semver": "^5.6.0" } }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -14764,22 +14759,15 @@ "find-up": "^3.0.0" } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "randombytes": "^2.1.0" } }, - "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14794,12 +14782,6 @@ "requires": { "figgy-pudding": "^3.5.1" } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true } } }, diff --git a/client/package.json b/client/package.json index 4ab14662..fdc19c9d 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,6 @@ "test:watch": "jest --watch" }, "dependencies": { - "@hot-loader/react-dom": "^16.13.0", "@nivo/line": "^0.49.1", "axios": "^0.19.2", "classnames": "^2.2.6", diff --git a/client/src/__locales/bg.json b/client/src/__locales/bg.json index 47668839..a5377391 100644 --- a/client/src/__locales/bg.json +++ b/client/src/__locales/bg.json @@ -139,8 +139,8 @@ "page_table_footer_text": "Страница", "rows_table_footer_text": "редове", "updated_custom_filtering_toast": "Обновени местни правила за филтриране", - "rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране", - "rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране", + "rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране: {{rule}}", + "rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}", "plain_dns": "Обикновен DNS", "source_label": "Източник", "found_in_known_domain_db": "Намерен в списъците с домейни.", diff --git a/client/src/__locales/cs.json b/client/src/__locales/cs.json index 5f38ba5f..1a593918 100644 --- a/client/src/__locales/cs.json +++ b/client/src/__locales/cs.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Stránka", "rows_table_footer_text": "řádky", "updated_custom_filtering_toast": "Aktualizovaná vlastní pravidla filtrování", - "rule_removed_from_custom_filtering_toast": "Pravidlo odstraněno z vlastních pravidel filtrování", - "rule_added_to_custom_filtering_toast": "Pravidlo přidáno do vlastních pravidel filtrování", + "rule_removed_from_custom_filtering_toast": "Pravidlo odstraněno z vlastních pravidel filtrování: {{rule}}", + "rule_added_to_custom_filtering_toast": "Pravidlo přidáno do vlastních pravidel filtrování: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrováno pomocí {{filter}}", "query_log_confirm_clear": "Opravdu chcete vymazat celý protokol dotazů?", @@ -564,4 +564,4 @@ "original_response": "Původní odezva", "click_to_view_queries": "Klikněte pro zobrazení dotazů", "port_53_faq_link": "Port 53 je často obsazen službami \"DNSStubListener\" nebo \"systemd-resolved\". Přečtěte si <0>tento návod o tom, jak to vyřešit." -} \ No newline at end of file +} diff --git a/client/src/__locales/da.json b/client/src/__locales/da.json index 915cd510..9470d91f 100644 --- a/client/src/__locales/da.json +++ b/client/src/__locales/da.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Side", "rows_table_footer_text": "rækker", "updated_custom_filtering_toast": "De brugerdefinerede filtreringsregler er blevet opdateret", - "rule_removed_from_custom_filtering_toast": "Regel fjernet fra de brugerdefinerede filtreringsregler", - "rule_added_to_custom_filtering_toast": "Regel tilføjet til de brugerdefinerede filtreringsregler", + "rule_removed_from_custom_filtering_toast": "Regel fjernet fra de brugerdefinerede filtreringsregler: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regel tilføjet til de brugerdefinerede filtreringsregler: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtreret af {{filter}}", "query_log_confirm_clear": "Er du sikker på, at du vil rydde hele forespørgselsloggen?", @@ -564,4 +564,4 @@ "original_response": "Oprindeligt svar", "click_to_view_queries": "Klik for at se forespørgsler", "port_53_faq_link": "Port 53 optages ofte af \"DNSStubListener\" eller \"systemd-resolved\" tjenester. Læs <0>denne instruktion om, hvordan du løser dette." -} \ No newline at end of file +} diff --git a/client/src/__locales/de.json b/client/src/__locales/de.json index fa6bf422..2a9784ad 100644 --- a/client/src/__locales/de.json +++ b/client/src/__locales/de.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Seite", "rows_table_footer_text": "Reihen", "updated_custom_filtering_toast": "Die benutzerdefinierten Filterregeln wurden aktualisiert", - "rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt", - "rule_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt", + "rule_removed_from_custom_filtering_toast": "Regel wurde aus den benutzerdefinierten Filterregeln entfernt: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regel wurde zu den benutzerdefinierten Filterregeln hinzugefügt: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Gefiltert nach {{filter}}", "query_log_confirm_clear": "Möchten Sie wirklich das Abfrageprotokoll vollständig löschen?", @@ -564,4 +564,4 @@ "original_response": "Ursprüngliche Antwort", "click_to_view_queries": "Anklicken, um Abfragen anzuzeigen", "port_53_faq_link": "Port 53 wird oft von Diensten wie „DNSStubListener” oder „systemresolved” belegt. Bitte lesen Sie <0>diese Anweisung, wie dies behoben werden kann." -} \ No newline at end of file +} diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 647ab6fa..e8e984e2 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -213,8 +213,8 @@ "page_table_footer_text": "Page", "rows_table_footer_text": "rows", "updated_custom_filtering_toast": "Updated the custom filtering rules", - "rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules", - "rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules", + "rule_removed_from_custom_filtering_toast": "Rule removed from the custom filtering rules: {{rule}}", + "rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtered by {{filter}}", "query_log_confirm_clear": "Are you sure you want to clear the entire query log?", diff --git a/client/src/__locales/es.json b/client/src/__locales/es.json index a63d33bb..8e7f23dd 100644 --- a/client/src/__locales/es.json +++ b/client/src/__locales/es.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Página", "rows_table_footer_text": "filas", "updated_custom_filtering_toast": "Reglas de filtrado personalizado actualizadas", - "rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado", - "rule_added_to_custom_filtering_toast": "Regla añadida a las reglas de filtrado personalizado", + "rule_removed_from_custom_filtering_toast": "Regla eliminada de las reglas de filtrado personalizado: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regla añadida a las reglas de filtrado personalizado: {{rule}}", "query_log_response_status": "Estado: {{value}}", "query_log_filtered": "Filtrado por {{filter}}", "query_log_confirm_clear": "¿Está seguro de que desea borrar todo el registro de consultas?", @@ -564,4 +564,4 @@ "original_response": "Respuesta original", "click_to_view_queries": "Clic para ver las consultas", "port_53_faq_link": "El puerto 53 suele estar ocupado por los servicios \"DNSStubListener\" o \"systemd-resolved\". Por favor lee <0>esta instrucción sobre cómo resolver esto." -} \ No newline at end of file +} diff --git a/client/src/__locales/fa.json b/client/src/__locales/fa.json index 81337001..ce668a00 100644 --- a/client/src/__locales/fa.json +++ b/client/src/__locales/fa.json @@ -202,8 +202,8 @@ "page_table_footer_text": "صفحه", "rows_table_footer_text": "سطر", "updated_custom_filtering_toast": "دستورات فیلترینگ دستی بروز رسانی شده است", - "rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد", - "rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد", + "rule_removed_from_custom_filtering_toast": "دستور از دستورات فیلترینگ دستی حذف شد {{rule}}", + "rule_added_to_custom_filtering_toast": "دستور به دستورات فیلترینگ دستی اضافه شد {{rule}}", "query_log_response_status": "وضعیت: {{value}}", "query_log_filtered": "فیلتر شده با {{filter}}", "query_log_confirm_clear": "آیا واقعا میخواهید کل وقایع جستار را پاک کنید؟", diff --git a/client/src/__locales/fr.json b/client/src/__locales/fr.json index b4ef7502..716595e8 100644 --- a/client/src/__locales/fr.json +++ b/client/src/__locales/fr.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Page", "rows_table_footer_text": "lignes", "updated_custom_filtering_toast": "Règles de filtrage d'utilisateur mises à jour", - "rule_removed_from_custom_filtering_toast": "Règle retirée des règles d'utilisateur", - "rule_added_to_custom_filtering_toast": "Règle ajoutée aux règles d'utilisateur", + "rule_removed_from_custom_filtering_toast": "Règle retirée des règles d'utilisateur: {{rule}}", + "rule_added_to_custom_filtering_toast": "Règle ajoutée aux règles d'utilisateur: {{rule}}", "query_log_response_status": "Statut : {{value}}", "query_log_filtered": "Filtré par {{filter}}", "query_log_confirm_clear": "Êtes-vous sûr de vouloir effacer tout le journal des requêtes ?", @@ -561,4 +561,4 @@ "filter_category_other_desc": "Autres listes noires", "click_to_view_queries": "Cliquez pour voir les requêtes", "port_53_faq_link": "Le port 53 est souvent occupé par les services « DNSStubListener » ou « systemd-resolved ». Veuillez lire <0>cette instruction pour savoir comment résoudre ce problème." -} \ No newline at end of file +} diff --git a/client/src/__locales/hr.json b/client/src/__locales/hr.json index 81c55105..28d93965 100644 --- a/client/src/__locales/hr.json +++ b/client/src/__locales/hr.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Stranica", "rows_table_footer_text": "redova", "updated_custom_filtering_toast": "Ažurirana su prilagođena pravila filtriranja", - "rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja", - "rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja", + "rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja: {{rule}}", + "rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrirao {{filter}}", "query_log_confirm_clear": "Jeste li sigurni da želite ukloniti zapise upita?", @@ -564,4 +564,4 @@ "original_response": "Originalni odgovor", "click_to_view_queries": "Kliknite za pregled upita", "port_53_faq_link": "Port 53 često zauzimaju usluge \"DNSStubListener\" ili \"systemd-resolved\". Molimo pročitajte <0>ove upute o tome kako to riješiti." -} \ No newline at end of file +} diff --git a/client/src/__locales/id.json b/client/src/__locales/id.json index 25b4a744..ca3918e2 100644 --- a/client/src/__locales/id.json +++ b/client/src/__locales/id.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Halaman", "rows_table_footer_text": "baris", "updated_custom_filtering_toast": "Perbarui aturan penyaringan khusus", - "rule_removed_from_custom_filtering_toast": "Aturan dihapus dari aturan penyaringan khusus", - "rule_added_to_custom_filtering_toast": "Aturan ditambah ke aturan penyaringan khusus", + "rule_removed_from_custom_filtering_toast": "Aturan dihapus dari aturan penyaringan khusus: {{rule}}", + "rule_added_to_custom_filtering_toast": "Aturan ditambah ke aturan penyaringan khusus: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Difilter oleh {{filter}}", "query_log_confirm_clear": "Apakah Anda yakin ingin menghapus seluruh kueri log?", @@ -564,4 +564,4 @@ "original_response": "Respon asli", "click_to_view_queries": "Klik untuk lihat permintaan", "port_53_faq_link": "Port 53 sering ditempati oleh layanan \"DNSStubListener\" atau \"systemd-resolved\". Silakan baca <0>instruksi ini tentang cara menyelesaikan ini." -} \ No newline at end of file +} diff --git a/client/src/__locales/it.json b/client/src/__locales/it.json index 83ad8ef0..d3ffa760 100644 --- a/client/src/__locales/it.json +++ b/client/src/__locales/it.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Pagina", "rows_table_footer_text": "righe", "updated_custom_filtering_toast": "Le regole dei filtri personalizzate sono state aggiornate", - "rule_removed_from_custom_filtering_toast": "Regola rimossa dalle regole dei filtri personalizzate", - "rule_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate", + "rule_removed_from_custom_filtering_toast": "Regola rimossa dalle regole dei filtri personalizzate: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regola aggiunta alle regole dei filtri personalizzate: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrato da {{filter}}", "query_log_confirm_clear": "Sei sicuro di voler eliminare la query log?", @@ -560,4 +560,4 @@ "filter_category_regional_desc": "Liste focalizzare su pubblicità regionali e server traccianti", "filter_category_other_desc": "Altre liste di blocco", "click_to_view_queries": "Clicca per visualizzare query" -} \ No newline at end of file +} diff --git a/client/src/__locales/ja.json b/client/src/__locales/ja.json index fb2930b2..89a44c7a 100644 --- a/client/src/__locales/ja.json +++ b/client/src/__locales/ja.json @@ -208,8 +208,8 @@ "page_table_footer_text": "ページ", "rows_table_footer_text": "行", "updated_custom_filtering_toast": "カスタム・フィルタリングルールを更新しました", - "rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました", - "rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました", + "rule_removed_from_custom_filtering_toast": "ルールをカスタム・フィルタリングルールから除去しました {{rule}}", + "rule_added_to_custom_filtering_toast": "ルールをカスタム・フィルタリングルールに追加しました {{rule}}", "query_log_response_status": "ステータス: {{value}}", "query_log_filtered": "{{filter}}によるフィルタ", "query_log_confirm_clear": "クエリ・ログ全体を消去してもよろしいですか?", @@ -564,4 +564,4 @@ "original_response": "当初の応答", "click_to_view_queries": "クエリを表示するにはクリックしてください", "port_53_faq_link": "多くの場合、ポート53は \"DNSStubListener\" または \"systemd-resolved\" サービスによって利用されています。これを解決する方法については、<0>この手順をお読みください。" -} \ No newline at end of file +} diff --git a/client/src/__locales/ko.json b/client/src/__locales/ko.json index 10244a34..cb06276e 100644 --- a/client/src/__locales/ko.json +++ b/client/src/__locales/ko.json @@ -208,8 +208,8 @@ "page_table_footer_text": "페이지", "rows_table_footer_text": "행", "updated_custom_filtering_toast": "사용자 정의 필터링 규칙 업데이트", - "rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거", - "rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙", + "rule_removed_from_custom_filtering_toast": "사용자 정의 필터링 규칙에서 규칙 제거 {{rule}}", + "rule_added_to_custom_filtering_toast": "사용자 정의 필터링 규칙에 추가된 규칙 {{rule}}", "query_log_response_status": "상태: {{value}}", "query_log_filtered": "필터: {{filter}}", "query_log_confirm_clear": "정말로 모든 쿼리 로그를 비우시겠습니까?", @@ -563,4 +563,4 @@ "filter_category_other_desc": "기타 차단 목록", "original_response": "원래 응답", "click_to_view_queries": "쿼리를 보려면 클릭합니다" -} \ No newline at end of file +} diff --git a/client/src/__locales/nl.json b/client/src/__locales/nl.json index 6877df79..1ae67d10 100644 --- a/client/src/__locales/nl.json +++ b/client/src/__locales/nl.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Pagina", "rows_table_footer_text": "rijen", "updated_custom_filtering_toast": "Aangepaste filter regels zijn bijgewerkt", - "rule_removed_from_custom_filtering_toast": "Regel verwijderd uit de aangepaste filterregels", - "rule_added_to_custom_filtering_toast": "Regel toegevoegd aan de aangepaste filterregels", + "rule_removed_from_custom_filtering_toast": "Regel verwijderd uit de aangepaste filterregels: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regel toegevoegd aan de aangepaste filterregels: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Gefilterd door {{filter}}", "query_log_confirm_clear": "Weet u zeker dat u het hele query logboek wilt legen?", @@ -564,4 +564,4 @@ "original_response": "Oorspronkelijke reactie", "click_to_view_queries": "Klik om queries te bekijken", "port_53_faq_link": "Poort 53 wordt vaak gebruikt door services als DNSStubListener- of de systeem DNS-resolver. Lees a.u.b. <0>deze instructie hoe dit is op te lossen." -} \ No newline at end of file +} diff --git a/client/src/__locales/no.json b/client/src/__locales/no.json index bcfad5fc..ebe5de11 100644 --- a/client/src/__locales/no.json +++ b/client/src/__locales/no.json @@ -207,8 +207,8 @@ "page_table_footer_text": "Side", "rows_table_footer_text": "rekker", "updated_custom_filtering_toast": "Oppdaterte de selvvalgte filtreringsreglene", - "rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene", - "rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene", + "rule_removed_from_custom_filtering_toast": "Oppføringen ble fjernet fra de selvvalgte filtreringsreglene: {{rule}}", + "rule_added_to_custom_filtering_toast": "Oppføringen ble lagt til i de selvvalgte filtreringsreglene: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrert av {{filter}}", "query_log_confirm_clear": "Er du sikker på at du vil slette hele forespørselsloggen?", diff --git a/client/src/__locales/pl.json b/client/src/__locales/pl.json index 8312b306..f00d3291 100644 --- a/client/src/__locales/pl.json +++ b/client/src/__locales/pl.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Strona", "rows_table_footer_text": "wierszy", "updated_custom_filtering_toast": "Zaktualizowano niestandardowe reguły filtrowania", - "rule_removed_from_custom_filtering_toast": "Reguła usunięta z niestandardowych reguł filtrowania", - "rule_added_to_custom_filtering_toast": "Reguła dodana do niestandardowych reguł filtrowania", + "rule_removed_from_custom_filtering_toast": "Reguła usunięta z niestandardowych reguł filtrowania: {{rule}}", + "rule_added_to_custom_filtering_toast": "Reguła dodana do niestandardowych reguł filtrowania: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrowane przez {{filter}}", "query_log_confirm_clear": "Czy na pewno chcesz wyczyścić cały dziennik zapytań?", @@ -564,4 +564,4 @@ "original_response": "Oryginalna odpowiedź", "click_to_view_queries": "Kliknij, aby wyświetlić zapytania", "port_53_faq_link": "Port 53 jest często zajęty przez usługi \"DNSStubListener\" lub \"systemd-resolved\". Przeczytaj <0>tę instrukcję jak to rozwiązać." -} \ No newline at end of file +} diff --git a/client/src/__locales/pt-br.json b/client/src/__locales/pt-br.json index 655d5c3e..92a73271 100644 --- a/client/src/__locales/pt-br.json +++ b/client/src/__locales/pt-br.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Página", "rows_table_footer_text": "linhas", "updated_custom_filtering_toast": "Regras de filtragem personalizadas atualizadas", - "rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas", - "rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas", + "rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrado por {{filter}}", "query_log_confirm_clear": "Você tem certeza que deseja limpar o registro de consulta?", @@ -560,4 +560,4 @@ "filter_category_regional_desc": "Listas focadas em anúncios regionais e servidores de rastreamento", "filter_category_other_desc": "Outras listas negras", "click_to_view_queries": "Clique para ver as consultas" -} \ No newline at end of file +} diff --git a/client/src/__locales/pt-pt.json b/client/src/__locales/pt-pt.json index 0b0b4cfe..7ee72444 100644 --- a/client/src/__locales/pt-pt.json +++ b/client/src/__locales/pt-pt.json @@ -168,8 +168,8 @@ "page_table_footer_text": "Página", "rows_table_footer_text": "linhas", "updated_custom_filtering_toast": "Regras de filtragem personalizadas actualizadas", - "rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas", - "rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas", + "rule_removed_from_custom_filtering_toast": "Regra removida das regras de filtragem personalizadas: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regra adicionada às regras de filtragem personalizadas: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrado por {{filter}}", "query_log_confirm_clear": "Tem a certeza de que deseja limpar todo o registo de consulta?", diff --git a/client/src/__locales/ro.json b/client/src/__locales/ro.json index 51f4c5b8..8c138d0b 100644 --- a/client/src/__locales/ro.json +++ b/client/src/__locales/ro.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Pagina", "rows_table_footer_text": "linii", "updated_custom_filtering_toast": "Reguli personalizate de filtrare aduse la zi", - "rule_removed_from_custom_filtering_toast": "Regulă scoasă din regullei personalizate de filtrare", - "rule_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate", + "rule_removed_from_custom_filtering_toast": "Regulă scoasă din regullei personalizate de filtrare: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate: {{rule}}", "query_log_response_status": "Statut: {{value}}", "query_log_filtered": "Filtrat de {{filter}}", "query_log_confirm_clear": "Sunteți sigur că doriți să ștergeți întregul jurnal de interogări?", @@ -564,4 +564,4 @@ "original_response": "Răspuns original", "click_to_view_queries": "Clicați pentru a vizualiza interogări", "port_53_faq_link": "Portul 53 este adesea ocupat de serviciile \"DNSStubListener\" sau \"systemd-resolved\". Vă rugăm să citiți <0>această instrucțiune despre cum să rezolvați aceasta." -} \ No newline at end of file +} diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json index eebd6db1..3f70b9b9 100644 --- a/client/src/__locales/ru.json +++ b/client/src/__locales/ru.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Страница", "rows_table_footer_text": "строк", "updated_custom_filtering_toast": "Внесены изменения в пользовательские правила", - "rule_removed_from_custom_filtering_toast": "Правило удалено из авторского списка правил фильтрации", - "rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено", + "rule_removed_from_custom_filtering_toast": "Пользовательское правило удалено: {{rule}}", + "rule_added_to_custom_filtering_toast": "Пользовательское правило добавлено: {{rule}}", "query_log_response_status": "Статус: {{value}}", "query_log_filtered": "Отфильтровано с помощью {{filter}}", "query_log_confirm_clear": "Вы уверены, что хотите очистить весь журнал запросов?", @@ -564,4 +564,4 @@ "original_response": "Первоначальный ответ", "click_to_view_queries": "Нажмите, чтобы просмотреть запросы", "port_53_faq_link": "Порт 53 часто занят службами \"DNSStubListener\" или \"systemd-resolved\". Ознакомьтесь с <0>инструкцией о том, как это разрешить." -} \ No newline at end of file +} diff --git a/client/src/__locales/sk.json b/client/src/__locales/sk.json index 5fd38e1c..2af3b14b 100644 --- a/client/src/__locales/sk.json +++ b/client/src/__locales/sk.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Stránka", "rows_table_footer_text": "riadky", "updated_custom_filtering_toast": "Aktualizované vlastné filtračné pravidlá", - "rule_removed_from_custom_filtering_toast": "Pravidlo odstránené z vlastných filtračných pravidiel", - "rule_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel", + "rule_removed_from_custom_filtering_toast": "Pravidlo odstránené z vlastných filtračných pravidiel: {{rule}}", + "rule_added_to_custom_filtering_toast": "Pravidlo pridané do vlastných filtračných pravidiel: {{rule}}", "query_log_response_status": "Stav: {{value}}", "query_log_filtered": "Vyfiltrované pomocou {{filter}}", "query_log_confirm_clear": "Naozaj chcete vymazať celý denník dopytov?", @@ -564,4 +564,4 @@ "original_response": "Pôvodná odozva", "click_to_view_queries": "Kliknite pre zobrazenie dopytov", "port_53_faq_link": "Port 53 je často obsadený službami \"DNSStubListener\" alebo \"systemd-resolved\". Prečítajte si <0>tento návod o tom, ako to vyriešiť." -} \ No newline at end of file +} diff --git a/client/src/__locales/sl.json b/client/src/__locales/sl.json index 4b0db8f2..8df6f369 100644 --- a/client/src/__locales/sl.json +++ b/client/src/__locales/sl.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Stran", "rows_table_footer_text": "vrstic", "updated_custom_filtering_toast": "Posodobljena pravila filtriranja po meri", - "rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil filtriranja po meri", - "rule_added_to_custom_filtering_toast": "Pravilo je dodano pravilom filtriranja po meri", + "rule_removed_from_custom_filtering_toast": "Pravilo je odstranjeno iz pravil filtriranja po meri: {{rule}}", + "rule_added_to_custom_filtering_toast": "Pravilo je dodano pravilom filtriranja po meri: {{rule}}", "query_log_response_status": "Stanje: {{value}}", "query_log_filtered": "Filtriran z {{filter}}", "query_log_confirm_clear": "Ali ste prepričani, da želite počistiti celoten dnevnik poizvedb?", @@ -564,4 +564,4 @@ "original_response": "Izviren odgovor", "click_to_view_queries": "Kliknite za prikaz poizvedb", "port_53_faq_link": "Vrata 53 pogosto zasedajo storitve 'DNSStubListener' ali 'Sistemsko razrešene storitve'. Preberite <0>to navodilo o tem, kako to rešiti." -} \ No newline at end of file +} diff --git a/client/src/__locales/sr-cs.json b/client/src/__locales/sr-cs.json index b9d0f054..d6b0ed49 100644 --- a/client/src/__locales/sr-cs.json +++ b/client/src/__locales/sr-cs.json @@ -208,8 +208,8 @@ "page_table_footer_text": "Stranica", "rows_table_footer_text": "redovi", "updated_custom_filtering_toast": "Ažurirana prilagođena pravila filtriranja", - "rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja", - "rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja", + "rule_removed_from_custom_filtering_toast": "Pravilo uklonjeno iz prilagođenih pravila filtriranja: {{rule}}", + "rule_added_to_custom_filtering_toast": "Pravilo dodato u prilagođena pravila filtriranja: {{rule}}", "query_log_response_status": "Stanje: {{value}}", "query_log_filtered": "Filtrirano od {{filter}}", "query_log_confirm_clear": "Jeste li sigurni da želite da očistite ceo dnevnik unosa?", @@ -564,4 +564,4 @@ "original_response": "Izvorni odgovor", "click_to_view_queries": "Kliknite da pogledate zahteve", "port_53_faq_link": "Port 53 je najčešće zauzet od \"DNSStubListener\" ili \"systemd-resolved\" usluga. Pročitajte <0>ovo uputstvo kako da to rešite." -} \ No newline at end of file +} diff --git a/client/src/__locales/sv.json b/client/src/__locales/sv.json index a20e467e..821bd369 100644 --- a/client/src/__locales/sv.json +++ b/client/src/__locales/sv.json @@ -162,8 +162,8 @@ "page_table_footer_text": "Sida", "rows_table_footer_text": "rader", "updated_custom_filtering_toast": "Uppdaterade de egna filterreglerna", - "rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna", - "rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna", + "rule_removed_from_custom_filtering_toast": "Regel borttagen från de egna filterreglerna: {{rule}}", + "rule_added_to_custom_filtering_toast": "Regel tillagd till de egna filterreglerna: {{rule}}", "query_log_response_status": "Status: {{value}}", "query_log_filtered": "Filtrerat av {{filter}}", "query_log_confirm_clear": "Är du säker på att du vill rensa hela förfrågningsloggen?", diff --git a/client/src/__locales/th.json b/client/src/__locales/th.json index 9aa4f98c..66494ac7 100644 --- a/client/src/__locales/th.json +++ b/client/src/__locales/th.json @@ -167,8 +167,8 @@ "page_table_footer_text": "หน้า", "rows_table_footer_text": "ตาราง", "updated_custom_filtering_toast": "อัปเดตกฎการกรองที่กำหนดเอง", - "rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว", - "rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว", + "rule_removed_from_custom_filtering_toast": "ลบกฎออกจากกฎการกรองที่กำหนดเองแล้ว {{rule}}", + "rule_added_to_custom_filtering_toast": "เพิ่มกฎในกฎการกรองที่กำหนดเองแล้ว {{rule}}", "query_log_response_status": "สถานะ: {{value}}", "query_log_filtered": "กรองโดย {{filter}}", "query_log_confirm_clear": "คุณแน่ใจหรือไม่ว่าต้องการลบบันทึกการใช้งานทั้งหมด?", diff --git a/client/src/__locales/tr.json b/client/src/__locales/tr.json index 000880a4..55e264e6 100644 --- a/client/src/__locales/tr.json +++ b/client/src/__locales/tr.json @@ -197,8 +197,8 @@ "page_table_footer_text": "Sayfa", "rows_table_footer_text": "satır", "updated_custom_filtering_toast": "İsteğe bağlı filtreleme kuralları güncellendi", - "rule_removed_from_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarından kaldırıldı", - "rule_added_to_custom_filtering_toast": "Kural isteğe bağlı filtreleme kurallarına eklendi", + "rule_removed_from_custom_filtering_toast": "Özel filtreleme kurallarından kural kaldırıldı: {{rule}}", + "rule_added_to_custom_filtering_toast": "Özel filtreleme kurallarına kural eklendi: {{rule}}", "query_log_response_status": "Durum: {{value}}", "query_log_filtered": "{{filter}} tarafından filtrelendi", "query_log_confirm_clear": "Tüm sorgu günlüğünü temizlemek istediğinizden emin misiniz?", @@ -507,4 +507,4 @@ "allowed": "İzin verildi", "blocklist": "Engellenen listesi", "port_53_faq_link": "Port 53 genellikle \"DNSStubListener\" veya \"sistemd-resolved\" hizmetler tarafından kullanılır. Lütfen problemin nasıl çözüleceğine ilişkin <0>bu talimatı okuyun." -} \ No newline at end of file +} diff --git a/client/src/__locales/vi.json b/client/src/__locales/vi.json index b878837a..b371d1db 100644 --- a/client/src/__locales/vi.json +++ b/client/src/__locales/vi.json @@ -172,8 +172,8 @@ "page_table_footer_text": "Trang", "rows_table_footer_text": "hàng", "updated_custom_filtering_toast": "Đã cập nhật quy tắc lọc tuỳ chỉnh", - "rule_removed_from_custom_filtering_toast": "Quy tắc đã được xoá khỏi quy tắc lọc tuỳ chỉnh", - "rule_added_to_custom_filtering_toast": "Quy tắc đã được thêm vào quy tắc lọc tuỳ chỉnh", + "rule_removed_from_custom_filtering_toast": "Quy tắc đã được xoá khỏi quy tắc lọc tuỳ chỉnh {{rule}}", + "rule_added_to_custom_filtering_toast": "Quy tắc đã được thêm vào quy tắc lọc tuỳ chỉnh: {{rule}}", "query_log_response_status": "Trạng thái: {{value}}", "query_log_filtered": "Được lọc bởi {{filter}}", "query_log_confirm_clear": "Bạn có chắc chắn muốn xóa toàn bộ nhật ký truy vấn không?", @@ -445,4 +445,4 @@ "blocked_threats": "Mối nguy hiểm đã chặn", "allowed": "Được phép", "safe_search": "Tìm kiếm an toàn" -} \ No newline at end of file +} diff --git a/client/src/__locales/zh-cn.json b/client/src/__locales/zh-cn.json index 86c543d6..3611816d 100644 --- a/client/src/__locales/zh-cn.json +++ b/client/src/__locales/zh-cn.json @@ -208,8 +208,8 @@ "page_table_footer_text": "页", "rows_table_footer_text": "行", "updated_custom_filtering_toast": "自定义过滤规则已更新", - "rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除", - "rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中", + "rule_removed_from_custom_filtering_toast": "规则已从自定义过滤规则列表中移除 {{rule}}", + "rule_added_to_custom_filtering_toast": "规则已添加到自定义过滤规则列表中 {{rule}}", "query_log_response_status": "状态: {{value}}", "query_log_filtered": "被 {{filter}} 过滤", "query_log_confirm_clear": "你确定想要清除全部查询日志吗?", @@ -564,4 +564,4 @@ "original_response": "原始响应", "click_to_view_queries": "点击查看查询", "port_53_faq_link": "53端口常被DNSStubListener或systemdn解析的服务占用。请阅读<0>这份关于如何解决这一问题的说明" -} \ No newline at end of file +} diff --git a/client/src/__locales/zh-tw.json b/client/src/__locales/zh-tw.json index f0f4a1df..9d2d62cb 100644 --- a/client/src/__locales/zh-tw.json +++ b/client/src/__locales/zh-tw.json @@ -208,8 +208,8 @@ "page_table_footer_text": "頁面", "rows_table_footer_text": "列", "updated_custom_filtering_toast": "已更新自訂的過濾規則", - "rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除", - "rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中", + "rule_removed_from_custom_filtering_toast": "規則從自訂的過濾規則中被移除 {{rule}}", + "rule_added_to_custom_filtering_toast": "規則被加至自訂的過濾規則中 {{rule}}", "query_log_response_status": "狀態:{{value}}", "query_log_filtered": "被 {{filter}} 過濾", "query_log_confirm_clear": "您確定您想要清除整個查詢記錄嗎?", @@ -564,4 +564,4 @@ "original_response": "原始的回應", "click_to_view_queries": "點擊以檢視查詢", "port_53_faq_link": "連接埠 53 常被 \"DNSStubListener\" 或 \"systemd-resolved\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明。" -} \ No newline at end of file +} diff --git a/client/src/actions/index.js b/client/src/actions/index.js index acb76107..ff512883 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -2,14 +2,17 @@ import { createAction } from 'redux-actions'; import i18next from 'i18next'; import axios from 'axios'; +import endsWith from 'lodash/endsWith'; +import escapeRegExp from 'lodash/escapeRegExp'; import { splitByNewLine, sortClients } from '../helpers/helpers'; import { - CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, + BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, } from '../helpers/constants'; import { areEqualVersions } from '../helpers/version'; import { getTlsStatus } from './encryption'; import apiClient from '../api/Api'; import { addErrorToast, addNoticeToast, addSuccessToast } from './toasts'; +import { getFilteringStatus, setRules } from './filtering'; export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE'); export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW'); @@ -541,3 +544,35 @@ export const removeStaticLease = (config) => async (dispatch) => { }; export const removeToast = createAction('REMOVE_TOAST'); + +export const toggleBlocking = (type, domain) => async (dispatch, getState) => { + const { userRules } = getState().filtering; + + const lineEnding = !endsWith(userRules, '\n') ? '\n' : ''; + const baseRule = `||${domain}^$important`; + const baseUnblocking = `@@${baseRule}`; + + const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule; + const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking; + const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`); + const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`); + + const matchPreparedBlockingRule = userRules.match(preparedBlockingRule); + const matchPreparedUnblockingRule = userRules.match(preparedUnblockingRule); + + if (matchPreparedBlockingRule) { + dispatch(setRules(userRules.replace(`${blockingRule}`, ''))); + dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule }))); + } else if (!matchPreparedUnblockingRule) { + dispatch(setRules(`${userRules}${lineEnding}${unblockingRule}\n`)); + dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); + } else if (matchPreparedUnblockingRule) { + dispatch(addSuccessToast(i18next.t('rule_added_to_custom_filtering_toast', { rule: unblockingRule }))); + return; + } else if (!matchPreparedBlockingRule) { + dispatch(addSuccessToast(i18next.t('rule_removed_from_custom_filtering_toast', { rule: blockingRule }))); + return; + } + + dispatch(getFilteringStatus()); +}; diff --git a/client/src/actions/queryLogs.js b/client/src/actions/queryLogs.js index bf3bee9f..bfb9a609 100644 --- a/client/src/actions/queryLogs.js +++ b/client/src/actions/queryLogs.js @@ -3,9 +3,7 @@ import { createAction } from 'redux-actions'; import apiClient from '../api/Api'; import { normalizeLogs, getParamsForClientsSearch, addClientInfo } from '../helpers/helpers'; import { - DEFAULT_LOGS_FILTER, - TABLE_DEFAULT_PAGE_SIZE, - TABLE_FIRST_PAGE, + DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT, } from '../helpers/constants'; import { addErrorToast, addSuccessToast } from './toasts'; @@ -37,15 +35,22 @@ export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUES export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE'); export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS'); -const checkFilteredLogs = async (data, filter, dispatch, total) => { +const shortPollQueryLogs = async (data, filter, dispatch, getState, total) => { const { logs, oldest } = data; const totalData = total || { logs }; - const needToGetAdditionalLogs = (logs.length < TABLE_DEFAULT_PAGE_SIZE - || totalData.logs.length < TABLE_DEFAULT_PAGE_SIZE) - && oldest !== ''; + const queryForm = getState().form[FORM_NAME.LOGS_FILTER]; + const currentQuery = queryForm && queryForm.values.search; + const previousQuery = filter?.search; + const isQueryTheSame = typeof previousQuery === 'string' + && typeof currentQuery === 'string' + && previousQuery === currentQuery; - if (needToGetAdditionalLogs) { + const isShortPollingNeeded = (logs.length < QUERY_LOGS_PAGE_LIMIT + || totalData.logs.length < QUERY_LOGS_PAGE_LIMIT) + && oldest !== '' && isQueryTheSame; + + if (isShortPollingNeeded) { dispatch(getAdditionalLogsRequest()); try { @@ -54,7 +59,7 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => { filter, }); if (additionalLogs.oldest.length > 0) { - return await checkFilteredLogs(additionalLogs, filter, dispatch, { + return await shortPollQueryLogs(additionalLogs, filter, dispatch, getState, { logs: [...totalData.logs, ...additionalLogs.logs], oldest: additionalLogs.oldest, }); @@ -71,31 +76,25 @@ const checkFilteredLogs = async (data, filter, dispatch, total) => { return totalData; }; -export const setLogsPagination = createAction('LOGS_PAGINATION'); -export const setLogsPage = createAction('SET_LOG_PAGE'); export const toggleDetailedLogs = createAction('TOGGLE_DETAILED_LOGS'); export const getLogsRequest = createAction('GET_LOGS_REQUEST'); export const getLogsFailure = createAction('GET_LOGS_FAILURE'); export const getLogsSuccess = createAction('GET_LOGS_SUCCESS'); -export const getLogs = (config) => async (dispatch, getState) => { +export const getLogs = () => async (dispatch, getState) => { dispatch(getLogsRequest()); try { - const { isFiltered, filter, page } = getState().queryLogs; + const { isFiltered, filter, oldest } = getState().queryLogs; const data = await getLogsWithParams({ - ...config, + older_than: oldest, filter, }); if (isFiltered) { - const additionalData = await checkFilteredLogs(data, filter, dispatch); + const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState); const updatedData = additionalData.logs ? { ...data, ...additionalData } : data; dispatch(getLogsSuccess(updatedData)); - dispatch(setLogsPagination({ - page, - pageSize: TABLE_DEFAULT_PAGE_SIZE, - })); } else { dispatch(getLogsSuccess(data)); } @@ -111,7 +110,7 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST'); * * @param filter * @param {string} filter.search - * @param {string} filter.response_status query field of RESPONSE_FILTER object + * @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object * @returns function */ export const setLogsFilter = (filter) => setLogsFilterRequest(filter); @@ -120,21 +119,20 @@ export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST'); export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE'); export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS'); -export const setFilteredLogs = (filter) => async (dispatch) => { +export const setFilteredLogs = (filter) => async (dispatch, getState) => { dispatch(setFilteredLogsRequest()); try { const data = await getLogsWithParams({ older_than: '', filter, }); - const additionalData = await checkFilteredLogs(data, filter, dispatch); + const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState); const updatedData = additionalData.logs ? { ...data, ...additionalData } : data; dispatch(setFilteredLogsSuccess({ ...updatedData, filter, })); - dispatch(setLogsPage(TABLE_FIRST_PAGE)); } catch (error) { dispatch(addErrorToast({ error })); dispatch(setFilteredLogsFailure(error)); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 62248fd0..86791162 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -1,7 +1,7 @@ import axios from 'axios'; import { getPathWithQueryString } from '../helpers/helpers'; -import { R_PATH_LAST_PART } from '../helpers/constants'; +import { QUERY_LOGS_PAGE_LIMIT, R_PATH_LAST_PART } from '../helpers/constants'; import { BASE_URL } from '../../constants'; class Api { @@ -530,6 +530,8 @@ class Api { getQueryLog(params) { const { path, method } = this.GET_QUERY_LOG; + // eslint-disable-next-line no-param-reassign + params.limit = QUERY_LOGS_PAGE_LIMIT; const url = getPathWithQueryString(path, params); return this.makeRequest(url, method); } diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js index ca2f13e2..1ca18e08 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -26,7 +26,6 @@ import Header from '../Header'; import { changeLanguage, getDnsStatus } from '../../actions'; import Dashboard from '../../containers/Dashboard'; -import Logs from '../../containers/Logs'; import SetupGuide from '../../containers/SetupGuide'; import Settings from '../../containers/Settings'; import Dns from '../../containers/Dns'; @@ -38,6 +37,7 @@ import DnsAllowlist from '../../containers/DnsAllowlist'; import DnsRewrites from '../../containers/DnsRewrites'; import CustomRules from '../../containers/CustomRules'; import Services from '../Filters/Services'; +import Logs from '../Logs'; const ROUTES = [ diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js index c6ac1c16..24b278f4 100644 --- a/client/src/components/Dashboard/Clients.js +++ b/client/src/components/Dashboard/Clients.js @@ -1,14 +1,17 @@ import React from 'react'; import ReactTable from 'react-table'; import PropTypes from 'prop-types'; -import { Trans, withTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import classNames from 'classnames'; import Card from '../ui/Card'; import Cell from '../ui/Cell'; import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers'; -import { IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants'; -import { formatClientCell } from '../../helpers/formatClientCell'; +import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants'; +import { toggleClientBlock } from '../../actions/access'; +import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell'; const getClientsPercentColor = (percent) => { if (percent > 50) { @@ -20,126 +23,131 @@ const getClientsPercentColor = (percent) => { return STATUS_COLORS.red; }; -const countCell = (dnsQueries) => function cell(row) { - const { value } = row; - const percent = getPercent(dnsQueries, value); +const CountCell = (row) => { + const { value, original: { ip } } = row; + const numDnsQueries = useSelector((state) => state.stats.numDnsQueries, shallowEqual); + + const percent = getPercent(numDnsQueries, value); const percentColor = getClientsPercentColor(percent); - return ; + return ; }; -const renderBlockingButton = (ipMatchListStatus, ip, handleClick, processing) => { - const buttonProps = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND - ? { - className: 'btn-outline-danger', - text: 'block', - type: 'block', +const renderBlockingButton = (ip) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const processingSet = useSelector((state) => state.access.processingSet); + const disallowed_clients = useSelector( + (state) => state.access.disallowed_clients, shallowEqual, + ); + + const ipMatchListStatus = getIpMatchListStatus(ip, disallowed_clients); + + if (ipMatchListStatus === IP_MATCH_LIST_STATUS.CIDR) { + return null; + } + + const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND; + const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK; + const text = type; + + const className = classNames('btn btn-sm', { + 'btn-outline-danger': isNotFound, + 'btn-outline-secondary': !isNotFound, + }); + + const toggleClientStatus = (type, ip) => { + const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock'; + + if (window.confirm(t(confirmMessage, { ip }))) { + dispatch(toggleClientBlock(type, ip)); } - : { - className: 'btn-outline-secondary', - text: 'unblock', - type: 'unblock', - }; + }; - return ( -
- -
- ); + const onClick = () => toggleClientStatus(type, ip); + + return
+ +
; }; -const clientCell = (t, toggleClientStatus, processing, disallowedClients) => function cell(row) { - const { value } = row; - const ipMatchListStatus = getIpMatchListStatus(value, disallowedClients); +const ClientCell = (row) => { + const { value, original: { info } } = row; - return ( - <> -
- {formatClientCell(row, true, false)} -
- {ipMatchListStatus !== IP_MATCH_LIST_STATUS.CIDR - && renderBlockingButton(ipMatchListStatus, value, toggleClientStatus, processing)} - - ); + return <> +
+ {renderFormattedClientCell(value, info, true)} + {renderBlockingButton(value)} +
+ ; }; const Clients = ({ - t, refreshButton, - topClients, subtitle, - dnsQueries, - toggleClientStatus, - processingAccessSet, - disallowedClients, -}) => ( - { + const { t } = useTranslation(); + const topClients = useSelector((state) => state.stats.topClients, shallowEqual); + const disallowedClients = useSelector((state) => state.access.disallowed_clients, shallowEqual); + + return ({ - ip, - count, - info, - blocked, - }))} - columns={[ - { - Header: 'IP', - accessor: 'ip', - sortMethod: sortIp, - Cell: clientCell(t, toggleClientStatus, processingAccessSet, disallowedClients), - }, - { - Header: requests_count, - accessor: 'count', - minWidth: 180, - maxWidth: 200, - Cell: countCell(dnsQueries), - }, - ]} - showPagination={false} - noDataText={t('no_clients_found')} - minRows={6} - defaultPageSize={100} - className="-highlight card-table-overflow--limited clients__table" - getTrProps={(_state, rowInfo) => { - if (!rowInfo) { - return {}; - } + data={topClients.map(({ + name: ip, count, info, blocked, + }) => ({ + ip, + count, + info, + blocked, + }))} + columns={[ + { + Header: 'IP', + accessor: 'ip', + sortMethod: sortIp, + Cell: ClientCell, + }, + { + Header: requests_count, + accessor: 'count', + minWidth: 180, + maxWidth: 200, + Cell: CountCell, + }, + ]} + showPagination={false} + noDataText={t('no_clients_found')} + minRows={6} + defaultPageSize={100} + className="-highlight card-table-overflow--limited clients__table" + getTrProps={(_state, rowInfo) => { + if (!rowInfo) { + return {}; + } - const { ip } = rowInfo.original; + const { ip } = rowInfo.original; - return getIpMatchListStatus(ip, disallowedClients) - === IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' }; - }} + return getIpMatchListStatus(ip, disallowedClients) === IP_MATCH_LIST_STATUS.NOT_FOUND ? {} : { className: 'red' }; + }} /> - -); - -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; diff --git a/client/src/components/Dashboard/Counters.js b/client/src/components/Dashboard/Counters.js index ecf35fbd..6b57484e 100644 --- a/client/src/components/Dashboard/Counters.js +++ b/client/src/components/Dashboard/Counters.js @@ -47,32 +47,32 @@ const Counters = ({ refreshButton, subtitle }) => { label: 'dns_query', count: numDnsQueries, tooltipTitle: interval === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: interval }), - response_status: RESPONSE_FILTER.ALL.query, + response_status: RESPONSE_FILTER.ALL.QUERY, }, { label: 'blocked_by', count: numBlockedFiltering, tooltipTitle: 'number_of_dns_query_blocked_24_hours', - response_status: RESPONSE_FILTER.BLOCKED.query, + response_status: RESPONSE_FILTER.BLOCKED.QUERY, translationComponents: [link], }, { label: 'stats_malware_phishing', count: numReplacedSafebrowsing, tooltipTitle: 'number_of_dns_query_blocked_24_hours_by_sec', - response_status: RESPONSE_FILTER.BLOCKED_THREATS.query, + response_status: RESPONSE_FILTER.BLOCKED_THREATS.QUERY, }, { label: 'stats_adult', count: numReplacedParental, tooltipTitle: 'number_of_dns_query_blocked_24_hours_adult', - response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.query, + response_status: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.QUERY, }, { label: 'enforced_save_search', count: numReplacedSafesearch, tooltipTitle: 'number_of_dns_query_to_safe_search', - response_status: RESPONSE_FILTER.SAFE_SEARCH.query, + response_status: RESPONSE_FILTER.SAFE_SEARCH.QUERY, }, { label: 'average_processing_time', diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js index 1aa426f8..07d8f94f 100644 --- a/client/src/components/Dashboard/index.js +++ b/client/src/components/Dashboard/index.js @@ -10,7 +10,6 @@ import BlockedDomains from './BlockedDomains'; import PageTitle from '../ui/PageTitle'; import Loading from '../ui/Loading'; -import { BLOCK_ACTIONS } from '../../helpers/constants'; import './Dashboard.css'; const Dashboard = ({ @@ -19,7 +18,6 @@ const Dashboard = ({ getStatsConfig, dashboard, toggleProtection, - toggleClientBlock, stats, access, }) => { @@ -50,14 +48,6 @@ const Dashboard = ({ ; }; - 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 = ; + }; + + return
+ +
+
+ {renderFormattedClientCell(client, info, isDetailed, true)} +
+ {isDetailed && name && !whoisAvailable + &&
+ {name} +
} +
+ {renderBlockingButton(isFiltered, domain)} +
; +}; + +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; diff --git a/client/src/components/Logs/Cells/DateCell.js b/client/src/components/Logs/Cells/DateCell.js new file mode 100644 index 00000000..b9552bc7 --- /dev/null +++ b/client/src/components/Logs/Cells/DateCell.js @@ -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
+
{formattedTime}
+ {isDetailed + &&
{formattedDate}
} +
; +}; + +DateCell.propTypes = { + time: propTypes.string.isRequired, +}; + +export default DateCell; diff --git a/client/src/components/Logs/Cells/getDomainCell.js b/client/src/components/Logs/Cells/DomainCell.js similarity index 55% rename from client/src/components/Logs/Cells/getDomainCell.js rename to client/src/components/Logs/Cells/DomainCell.js index 5de05e69..4333089c 100644 --- a/client/src/components/Logs/Cells/getDomainCell.js +++ b/client/src/components/Logs/Cells/DomainCell.js @@ -1,7 +1,8 @@ import React from 'react'; +import { useSelector } from 'react-redux'; import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import getIconTooltip from './getIconTooltip'; +import propTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; import { DEFAULT_SHORT_DATE_FORMAT_OPTIONS, LONG_TIME_FORMAT, @@ -9,15 +10,19 @@ import { } from '../../../helpers/constants'; import { captitalizeWords, formatDateTime, formatTime } from '../../../helpers/helpers'; import { getSourceData } from '../../../helpers/trackers/trackers'; +import IconTooltip from './IconTooltip'; -const getDomainCell = (props) => { - const { - row, t, isDetailed, dnssec_enabled, - } = props; - - const { - tracker, type, answer_dnssec, client_proto, domain, time, - } = row.original; +const DomainCell = ({ + answer_dnssec, + client_proto, + domain, + time, + tracker, + type, +}) => { + const { t } = useTranslation(); + const dnssec_enabled = useSelector((state) => state.dnsConfig.dnssec_enabled); + const isDetailed = useSelector((state) => state.queryLogs.isDetailed); const hasTracker = !!tracker; @@ -50,8 +55,8 @@ const getDomainCell = (props) => { name_table_header: tracker?.name, category_label: hasTracker && captitalizeWords(tracker.category), source_label: sourceData - &&
{sourceData.name}, + && {sourceData.name}, }; const renderGrid = (content, idx) => { @@ -72,51 +77,42 @@ const getDomainCell = (props) => { const renderContent = hasTracker ? requestDetails.concat(getGrid(knownTrackerDataObj, 'known_tracker', 'pt-4')) : requestDetails; - const trackerHint = getIconTooltip({ - className: privacyIconClass, - tooltipClass: 'pt-4 pb-5 px-5 mw-75', - xlinkHref: 'privacy', - contentItemClass: 'key-colon', - renderContent, - place: 'bottom', - }); - - const valueClass = classNames('w-100', { + const valueClass = classNames('w-100 text-truncate', { 'px-2 d-flex justify-content-center flex-column': isDetailed, }); const details = [ip, protocol].filter(Boolean) .join(', '); - return ( -
- {dnssec_enabled && getIconTooltip({ - className: lockIconClass, - tooltipClass: 'py-4 px-5 pb-45', - canShowTooltip: answer_dnssec, - xlinkHref: 'lock', - columnClass: 'w-100', - content: 'validated_with_dnssec', - placement: 'bottom', - })} - {trackerHint} -
-
{domain}
- {details && isDetailed - &&
{details}
} -
+ return
+ {dnssec_enabled && } + +
+
{domain}
+ {details && isDetailed + &&
{details}
}
- ); +
; }; -getDomainCell.propTypes = { - row: PropTypes.object.isRequired, - t: PropTypes.func.isRequired, - isDetailed: PropTypes.bool.isRequired, - toggleBlocking: PropTypes.func.isRequired, - autoClients: PropTypes.array.isRequired, - dnssec_enabled: PropTypes.bool.isRequired, +DomainCell.propTypes = { + answer_dnssec: propTypes.bool.isRequired, + client_proto: propTypes.string.isRequired, + domain: propTypes.string.isRequired, + time: propTypes.string.isRequired, + type: propTypes.string.isRequired, + tracker: propTypes.object, }; -export default getDomainCell; +export default DomainCell; diff --git a/client/src/components/Logs/Cells/Header.js b/client/src/components/Logs/Cells/Header.js new file mode 100644 index 00000000..4d20d055 --- /dev/null +++ b/client/src/components/Logs/Cells/Header.js @@ -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')} + { + + {t('compact')} + + + {t('default')} + + + } + , + }, + ]; + + return
+ {HEADERS.map(HeaderCell)} +
; +}; + +export default Header; diff --git a/client/src/components/Logs/Cells/HeaderCell.js b/client/src/components/Logs/Cells/HeaderCell.js new file mode 100644 index 00000000..b16f1390 --- /dev/null +++ b/client/src/components/Logs/Cells/HeaderCell.js @@ -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
+ {typeof content === 'string' ? t(content) : content} +
; +}; + +HeaderCell.propTypes = { + content: propTypes.oneOfType([propTypes.string, propTypes.element]).isRequired, + className: propTypes.string, +}; + +export default HeaderCell; diff --git a/client/src/components/Logs/Cells/getIconTooltip.js b/client/src/components/Logs/Cells/IconTooltip.js similarity index 93% rename from client/src/components/Logs/Cells/getIconTooltip.js rename to client/src/components/Logs/Cells/IconTooltip.js index 3f5327cd..5b9cc2cb 100644 --- a/client/src/components/Logs/Cells/getIconTooltip.js +++ b/client/src/components/Logs/Cells/IconTooltip.js @@ -7,7 +7,7 @@ import Tooltip from '../../ui/Tooltip'; import 'react-popper-tooltip/dist/styles.css'; import './IconTooltip.css'; -const getIconTooltip = ({ +const IconTooltip = ({ className, contentItemClass, columnClass, @@ -43,14 +43,14 @@ const getIconTooltip = ({ ; }; -getIconTooltip.propTypes = { +IconTooltip.propTypes = { className: PropTypes.string, contentItemClass: PropTypes.string, columnClass: PropTypes.string, tooltipClass: PropTypes.string, title: PropTypes.string, placement: PropTypes.string, - canShowTooltip: PropTypes.string, + canShowTooltip: PropTypes.bool, xlinkHref: PropTypes.string, content: PropTypes.oneOfType([ PropTypes.string, @@ -59,4 +59,4 @@ getIconTooltip.propTypes = { renderContent: PropTypes.arrayOf(PropTypes.element), }; -export default getIconTooltip; +export default IconTooltip; diff --git a/client/src/components/Logs/Cells/ResponseCell.js b/client/src/components/Logs/Cells/ResponseCell.js new file mode 100644 index 00000000..b152987f --- /dev/null +++ b/client/src/components/Logs/Cells/ResponseCell.js @@ -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 = {statusLabel}; + const filter = getFilterName(filters, whitelistFilters, filterId); + + const renderResponses = (responseArr) => { + if (!responseArr || responseArr.length === 0) { + return ''; + } + + return
{responseArr.map((response) => { + const className = classNames('white-space--nowrap', { + 'overflow-break': response.length > 100, + }); + + return
{`${response}\n`}
; + })}
; + }; + + 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
+ +
+
{statusLabel}
+ {isDetailed &&
{detailedInfo}
} +
+
; +}; + +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; diff --git a/client/src/components/Logs/Cells/getClientCell.js b/client/src/components/Logs/Cells/getClientCell.js deleted file mode 100644 index 6dbc9367..00000000 --- a/client/src/components/Logs/Cells/getClientCell.js +++ /dev/null @@ -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 ( -
- -
- ); - }; - - return ( -
- {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', - })} -
-
- {formatClientCell(row, isDetailed)} -
- - {isDetailed && name && !whoisAvailable && ( -
- {name} -
- )} -
- {renderBlockingButton(isFiltered, domain)} -
- ); -}; - -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; diff --git a/client/src/components/Logs/Cells/getDateCell.js b/client/src/components/Logs/Cells/getDateCell.js deleted file mode 100644 index 1b00133b..00000000 --- a/client/src/components/Logs/Cells/getDateCell.js +++ /dev/null @@ -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 ( -
-
{formattedTime}
- {isDetailed &&
{formattedDate}
} -
- ); -}; - -export default getDateCell; diff --git a/client/src/components/Logs/Cells/getResponseCell.js b/client/src/components/Logs/Cells/getResponseCell.js deleted file mode 100644 index 9bb1912a..00000000 --- a/client/src/components/Logs/Cells/getResponseCell.js +++ /dev/null @@ -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 = {statusLabel}; - const filter = getFilterName(filters, whitelistFilters, filterId, t); - - const renderResponses = (responseArr) => { - if (responseArr?.length === 0) { - return ''; - } - - return
{responseArr.map((response) => { - const className = classNames('white-space--nowrap', { - 'overflow-break': response.length > 100, - }); - - return
{`${response}\n`}
; - })}
; - }; - - 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 ( -
- {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', - })} -
-
{statusLabel}
- {isDetailed &&
{detailedInfo}
} -
-
- ); -}; - -export default getResponseCell; diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js new file mode 100644 index 00000000..c2d7968b --- /dev/null +++ b/client/src/components/Logs/Cells/index.js @@ -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 + ?
{requestStatus}
: 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 + && {sourceData.name} + , + 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]:
{t(buttonType)}
, + }; + + 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
+ + + + +
; +}); + +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; diff --git a/client/src/components/Logs/Filters/Form.js b/client/src/components/Logs/Filters/Form.js index 9c000cd4..0f9370f8 100644 --- a/client/src/components/Logs/Filters/Form.js +++ b/client/src/components/Logs/Filters/Form.js @@ -107,7 +107,7 @@ const Form = (props) => { const { response_status, search, - } = useSelector((state) => state.form[FORM_NAME.LOGS_FILTER].values, shallowEqual); + } = useSelector((state) => state?.form[FORM_NAME.LOGS_FILTER].values, shallowEqual); const [ debouncedSearch, @@ -171,14 +171,14 @@ const Form = (props) => { > {Object.values(RESPONSE_FILTER) .map(({ - query, label, disabled, + QUERY, LABEL, disabled, }) => ( )) } @@ -197,5 +197,4 @@ Form.propTypes = { export default reduxForm({ form: FORM_NAME.LOGS_FILTER, - enableReinitialize: true, })(Form); diff --git a/client/src/components/Logs/Filters/index.js b/client/src/components/Logs/Filters/index.js index fae8b06c..49465b44 100644 --- a/client/src/components/Logs/Filters/index.js +++ b/client/src/components/Logs/Filters/index.js @@ -1,10 +1,21 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; import Form from './Form'; +import { refreshFilteredLogs } from '../../../actions/queryLogs'; +import { addSuccessToast } from '../../../actions/toasts'; -const Filters = ({ filter, refreshLogs, setIsLoading }) => { +const Filters = ({ filter, setIsLoading }) => { const { t } = useTranslation(); + const dispatch = useDispatch(); + + const refreshLogs = async () => { + setIsLoading(true); + await dispatch(refreshFilteredLogs()); + dispatch(addSuccessToast('query_log_updated')); + setIsLoading(false); + }; return

@@ -29,7 +40,6 @@ const Filters = ({ filter, refreshLogs, setIsLoading }) => { Filters.propTypes = { filter: PropTypes.object.isRequired, - refreshLogs: PropTypes.func.isRequired, processingGetLogs: PropTypes.bool.isRequired, setIsLoading: PropTypes.func.isRequired, }; diff --git a/client/src/components/Logs/InfiniteTable.js b/client/src/components/Logs/InfiniteTable.js new file mode 100644 index 00000000..bced3340 --- /dev/null +++ b/client/src/components/Logs/InfiniteTable.js @@ -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) => ; + + const isNothingFound = items.length === 0 && !processingGetLogs; + + return
+ {loading && } +
+ {isNothingFound + ? + : <>{items.map(renderRow)} + {!isEntireLog &&
{t('loading_table_status')}
} + } +
; +}; + +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; diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 9df0a31b..857fd466 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -1,44 +1,21 @@ :root { + --blue: #e5effd; + --green-pale: rgba(103, 178, 121, 0.1); + --red: rgba(223, 56, 18, 0.05); + --white: #fff; + --yellow: rgba(247, 181, 0, 0.1); + --size-date: 70; + --size-domain: 180; + --size-response: 150; + --size-client: 123; + --gray-216: rgba(216, 216, 216, 0.23); --gray-4d: #4D4D4D; --gray-8: #888; --danger: #DF3812; + --white80: rgba(255, 255, 255, 0.8); } -.logs__row { - position: relative; - display: flex; - min-height: 26px; - overflow: hidden; - text-overflow: ellipsis; -} - -.card-table .logs__row { - overflow: hidden; - text-overflow: ellipsis; -} - -.logs__row--center { - justify-content: center; -} - -.logs__row--column { - flex-direction: column; - align-items: flex-start; - justify-content: center; -} - -.logs__row--icons { - max-width: 180px; - flex-flow: row wrap; -} - -.logs__row .list-unstyled { - margin-bottom: 0; - overflow: hidden; -} - -.logs__text, -.logs__row .list-unstyled li { +.logs__text { padding: 0 1px; text-overflow: ellipsis; white-space: nowrap; @@ -54,237 +31,6 @@ font-weight: bold; } -.logs__text--full { - width: 100%; -} - -.logs__text--wrap { - line-height: 1.4; - white-space: normal; -} - -.logs__text--nowrap { - line-height: 1.4; - white-space: nowrap; -} - -.logs__text--whois { - line-height: 1.2; - color: #9aa0ac; -} - -.logs__row .tooltip-custom { - top: 0; - margin-left: 0; - margin-right: 5px; -} - -.tooltip__option { - height: 2.5rem !important; - width: 10.5rem; - padding: 0.3125rem 1.5rem 0.6875rem; -} - -.tooltip__option:hover { - background-color: var(--gray-f3); - cursor: pointer; -} - -.button__action { - background-color: #fff; - border-radius: 4px; - transition: opacity 0.2s ease, visibility 0.2s ease; - visibility: hidden; - opacity: 0; -} - -.table__action { - position: absolute; - top: 11px; - right: 15px; -} - -.logs__action { - position: absolute; - top: 0; - right: 1rem; -} - -.logs__action--detailed { - top: 5px; -} - -.logs__table .rt-td, -.clients__table .rt-td { - position: relative; -} - -.logs__table .rt-thead, .logs__table .rt-tbody { - min-width: 100% !important; -} - -.logs__table .rt-tr:hover .logs__action, -.clients__table .rt-tr:hover .table__action { - visibility: visible; - opacity: 1; -} - -.logs__table .rt-tr-group:first-child .tooltip-custom:before { - top: calc(100% + 12px); - bottom: initial; - z-index: 1; -} - -.logs__table .rt-tr-group:first-child .tooltip-custom:after { - top: initial; - bottom: -4px; - border-top: 6px solid transparent; - border-bottom: 6px solid #585965; -} - -.logs__table .rt-tr-group:first-child .popover__body { - top: calc(100% + 5px); - bottom: initial; - z-index: 1; -} - -.logs__table .rt-tr-group:first-child .popover__body:after { - top: -11px; - border-top: 6px solid transparent; - border-bottom: 6px solid #585965; -} - -.logs__table .rt-thead.-filters input, -.logs__table .rt-thead.-filters select { - padding: 6px 7px; - border-radius: 3px; - font-size: 0.9375rem; - line-height: 1.6; - color: #495057; - border: 1px solid rgba(0, 40, 100, 0.12); -} - -.logs__table .rt-thead.-filters select { - background: #fff url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxMCA1Jz48cGF0aCBmaWxsPScjOTk5JyBkPSdNMCAwTDEwIDBMNSA1TDAgMCcvPjwvc3ZnPg==") no-repeat right 0.75rem center; - background-size: 8px 10px; -} - -.logs__table .rt-thead.-filters input:focus, -.logs__table .rt-thead.-filters select:focus { - border-color: #1991eb; - box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25); -} - -.logs__text-wrap { - display: flex; - align-items: center; - max-width: 100%; -} - -.logs__list-wrap { - display: flex; - max-width: 100%; -} - -.logs__list-item { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -.logs__input-wrap { - position: relative; -} - -.logs__whois { - display: inline; - font-size: 12px; - white-space: nowrap; -} - -.logs__whois::after { - content: "|"; - padding: 0 5px; - opacity: 0.3; -} - -.logs__whois:last-child::after { - content: ""; -} - -.logs__whois-icon.icons { - position: relative; - top: -2px; - width: 12px; - height: 12px; - margin-right: 1px; - opacity: 0.5; -} - -/* New logs */ -.logs__table { - background-color: #fff; - border: 0; - border-radius: 8px; - min-height: 42rem; - max-width: 100%; -} - -.logs__table--detailed { - min-height: 50rem; -} - -.logs__table .rt-thead.-header { - box-shadow: none; - font-weight: bold; -} - -.logs__table .rt-thead .rt-th { - padding: 0.9375rem 0.9375rem 0.875rem 0; - text-align: left; - border-right: 0; -} - -.logs__table .rt-tbody .rt-td { - padding: 1rem 1rem 0.5rem 0; - border-right: 0; -} - -.logs__table .rt-thead .rt-th:last-child, -.logs__table .rt-tbody .rt-td:last-child { - padding-right: 0; -} - -.logs__table .rt-tbody .rt-tr-group { - border-bottom: 0; -} - -.logs__table .rt-tr { - position: relative; - padding: 0 24px; -} - -.logs__table .rt-tr { - position: relative; - padding: 0 1.5rem; -} - -.logs__table .rt-tr-group:not(:first-child) .rt-tr:before { - content: ""; - position: absolute; - left: 1.5rem; - right: 1.5rem; - top: 0; - width: calc(100% - 3rem); - height: 2px; - background-color: rgba(216, 216, 216, 0.23); -} - -.logs__table .rt-tr-group:last-child .rt-tr:after, -.logs__table .rt-thead .rt-tr:after { - display: none; -} - .logs__time { font-size: 1rem; line-height: 1.5; @@ -302,132 +48,24 @@ border-radius: 4px; } -/* Hide 3 and 4 column on mobile */ -.logs__table .rt-thead .rt-th:nth-child(3), -.logs__table .rt-thead .rt-th:nth-child(4), -.logs__table .rt-tbody .rt-td:nth-child(3), -.logs__table .rt-tbody .rt-td:nth-child(4) { - display: none; -} - -@media screen and (min-width: 768px) { - .logs__table .rt-thead .rt-th:nth-child(3), - .logs__table .rt-thead .rt-th:nth-child(4), - .logs__table .rt-tbody .rt-td:nth-child(3), - .logs__table .rt-tbody .rt-td:nth-child(4) { - display: block; - } -} - .text-pre { white-space: pre-wrap !important; overflow-wrap: break-word; overflow: visible; } -.custom-pagination { - width: 11.875rem !important; - background-color: transparent; - box-shadow: none !important; - border: none !important; - align-items: center !important; -} - -.custom-pagination--padding { - padding: 2.5rem 0 2.5rem !important; -} - -.custom-pagination .-btn { - --side-size: 2rem; - background-color: transparent !important; - border: 1px solid var(--gray-d8) !important; - border-radius: 4px !important; - width: var(--side-size) !important; - height: var(--side-size) !important; -} - -.custom-pagination .-btn:enabled:hover { - background-color: var(--gray-f3) !important; -} - -.custom-pagination .-previous { - flex: 0 1 !important; -} - -.custom-pagination .-next { - flex: 0 1 !important; -} - -.custom-pagination .-btn { - display: flex !important; -} - -.logs__table .-pageInfo { - --side-size: 2rem; - font-variant-numeric: tabular-nums !important; - background-color: transparent !important; - border: 1px solid var(--gray-d8) !important; - border-radius: 4px !important; - width: var(--side-size) !important; - height: var(--side-size) !important; - margin: 0 !important; - display: flex !important; - justify-content: center; - align-items: center; -} - -.logs__table .pagination-bottom { - justify-content: center !important; - display: flex !important; -} - -.logs__table .-center:before { - content: '...'; - transform: translateY(-0.25rem); - margin: auto; -} - -.logs__table .-center:after { - content: '...'; - transform: translateY(-0.25rem); - margin: auto; -} - -.icon--detailed-info { - position: absolute; - right: 0; - top: 0.5rem; -} - .link--green { color: var(--green79); } -.row--detailed { - height: 4.9rem -} - .w-90 { max-width: 90% !important; } -.h-85 { - height: 85% !important; -} - -.pt-45 { - padding-top: 1.25rem !important; -} - .pb-45 { padding-bottom: 1.25rem !important; } -.py-45 { - padding-top: 1.25rem !important; - padding-bottom: 1.25rem !important; -} - .mh-100 { max-height: 100% !important; } @@ -493,14 +131,6 @@ } @media (max-width: 767.98px) { - .rt-tr .logs__row .logs__text { - max-width: calc(100% - 1.5rem); - } - - .ml-small { - margin-left: 1.5rem; - } - .form-control--container { width: 100%; flex-direction: column; @@ -517,38 +147,157 @@ } } -@media (max-width: 575px) { - .logs__table .rt-tr { - height: 3.125rem; +@media screen and (max-width: 767.98px) { + .logs__table .logs__cell--response, + .logs__table .logs__cell--client { + display: none !important; } - - .logs__table .rt-tbody .rt-td { - padding: 0.625rem 1rem 0.875rem 0; - } - - .logs__table { - min-height: 42rem; - } -} - -.loading__container > .-loading-inner { - top: 10rem !important; - bottom: initial !important; -} - -.loading__text { - transform: translateY(3rem); } .logs__refresh { + --size: 2.5rem; position: relative; top: 3px; display: inline-flex; align-items: center; justify-content: center; - width: 40px; - height: 40px; + width: var(--size); + height: var(--size); padding: 0; - margin-left: 15px; + margin-left: 0.9375rem; background-color: transparent; } + +.logs__cell { + padding: 1rem 1rem 0.5rem 0; +} + +.logs__cell--date { + width: 4.375rem; + flex: var(--size-date) 0 auto; +} + +.logs__cell--domain { + width: 11.25rem; + flex: var(--size-domain) 0 auto; +} + +.logs__cell--response { + width: 9.375rem; + flex: var(--size-response) 0 auto; +} + +.logs__cell--client { + width: 7.6875rem; + flex: var(--size-client) 0 auto; + padding-right: 0; +} + +.logs__cell--header__container > .logs__cell--header__item { + border-right: 0; + font-size: 1rem; +} + +.logs__cell--header__container > .logs__cell--header__item:last-child { + padding-right: 0; +} + +.logs__cell--block-button { + max-height: 1.75rem; + position: relative; + left: 10%; + top: 40%; + visibility: hidden; +} + +.logs__row { + position: relative; + display: flex; + min-height: 26px; + overflow: hidden; + text-overflow: ellipsis; +} + +.logs__table .logs__row { + border-bottom: 2px solid var(--gray-216); +} + +.logs__table .logs__row:hover .logs__cell--block-button { + visibility: visible; +} + +.logs__table .logs__row .logs__cell--block-button:disabled { + background-color: var(--white) !important; +} + +/* QUERY_STATUS_COLORS */ +.logs__row--blue { + background-color: var(--blue); +} + +.logs__row--green { + background-color: var(--green-pale); +} + +.logs__row--red { + background-color: var(--red); +} + +.logs__row--white { + background-color: var(--white); +} + +.logs__row--yellow { + background-color: var(--yellow); +} + +.logs__no-data { + color: var(--gray-4d); + background-color: var(--white80); + pointer-events: none; + font-weight: bold; + text-align: center; + padding-top: 21rem; + display: block; +} + +.logs__loading { + padding: 1rem 0; +} + +.logs__table { + background-color: var(--white); + border: 0; + border-radius: 8px; + min-height: 43rem; + max-width: 100%; + align-items: stretch; + width: 100%; + border-collapse: collapse; + contain: layout; + overflow-x: hidden; + overflow-y: scroll; + will-change: scroll-position; +} + +.logs__table .logs__cell--response, +.logs__table .logs__cell--client { + display: flex; +} + +.logs__cell--header__container { + display: flex; +} + +.logs__table > .logs__cell--header__container > .logs__cell--client { + display: flex; + justify-content: space-between; +} + +.logs__table .loading:after { + top: 10%; +} + +.logs__table .loading:before { + min-height: 100%; +} diff --git a/client/src/components/Logs/Table.js b/client/src/components/Logs/Table.js deleted file mode 100644 index ea648426..00000000 --- a/client/src/components/Logs/Table.js +++ /dev/null @@ -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
- {t('client_table_header')} - { - toggleDetailedLogs(false)} - > - compact - - - toggleDetailedLogs(true)} - > - default - - - } -
; - }, - 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 ( - - -
{t('loading_table_status')}
- - } - getLoadingProps={() => ({ className: 'loading__container' })} - rowsText={t('rows_table_footer_text')} - noDataText={!processingGetLogs - && } - pageText='' - ofText='' - showPagination={logs.length > 0} - getPaginationProps={() => ({ className: 'custom-pagination custom-pagination--padding' })} - getTbodyProps={() => ({ className: 'd-block' })} - previousText={ - - previous_btn - - } - nextText={ - - next_btn - - } - 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 - ?
{requestStatus}
: 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 - && {sourceData.name} - , - 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]:
{t(buttonType)}
, - }; - - 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; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 8777a0a3..13fa697c 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -1,5 +1,4 @@ -import React, { Fragment, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; import { Trans } from 'react-i18next'; import Modal from 'react-modal'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; @@ -8,24 +7,21 @@ import queryString from 'query-string'; import classNames from 'classnames'; import { BLOCK_ACTIONS, - TABLE_DEFAULT_PAGE_SIZE, - TABLE_FIRST_PAGE, SMALL_SCREEN_SIZE, } from '../../helpers/constants'; import Loading from '../ui/Loading'; import Filters from './Filters'; -import Table from './Table'; import Disabled from './Disabled'; import { getFilteringStatus } from '../../actions/filtering'; import { getClients } from '../../actions'; import { getDnsConfig } from '../../actions/dnsConfig'; import { getLogsConfig, - refreshFilteredLogs, resetFilteredLogs, setFilteredLogs, + toggleDetailedLogs, } from '../../actions/queryLogs'; -import { addSuccessToast } from '../../actions/toasts'; +import InfiniteTable from './InfiniteTable'; import './Logs.css'; const processContent = (data, buttonType) => Object.entries(data) @@ -48,21 +44,20 @@ const processContent = (data, buttonType) => Object.entries(data) keyClass = ''; } - return isHidden ? null : + return isHidden ? null :
+ className={classNames(`key__${key}`, keyClass, { + 'font-weight-bold': isBoolean && value === true, + })}> {isButton ? value : key}
{(isTitle || isButton || isBoolean) ? '' : value || '—'}
- ; +
; }); - -const Logs = (props) => { +const Logs = () => { const dispatch = useDispatch(); const history = useHistory(); @@ -71,7 +66,14 @@ const Logs = (props) => { search: search_url_param = '', } = queryString.parse(history.location.search); - const { filter } = useSelector((state) => state.queryLogs, shallowEqual); + const { + enabled, + processingGetConfig, + processingAdditionalLogs, + processingGetLogs, + } = useSelector((state) => state.queryLogs, shallowEqual); + const filter = useSelector((state) => state.queryLogs.filter, shallowEqual); + const logs = useSelector((state) => state.queryLogs.logs, shallowEqual); const search = filter?.search || search_url_param; const response_status = filter?.response_status || response_status_url_param; @@ -82,6 +84,7 @@ const Logs = (props) => { const [isModalOpened, setModalOpened] = useState(false); const [isLoading, setIsLoading] = useState(false); + const closeModal = () => setModalOpened(false); useEffect(() => { (async () => { @@ -94,44 +97,11 @@ const Logs = (props) => { })(); }, [response_status, search]); - const { - filtering, - setLogsPage, - setLogsPagination, - toggleDetailedLogs, - dashboard, - dnsConfig, - queryLogs: { - enabled, - processingGetConfig, - processingAdditionalLogs, - processingGetLogs, - oldest, - logs, - pages, - page, - isDetailed, - }, - } = props; - const mediaQuery = window.matchMedia(`(max-width: ${SMALL_SCREEN_SIZE}px)`); const mediaQueryHandler = (e) => { setIsSmallScreen(e.matches); if (e.matches) { - toggleDetailedLogs(false); - } - }; - - const closeModal = () => setModalOpened(false); - - const getLogs = (older_than, page, initial) => { - if (enabled) { - props.getLogs({ - older_than, - page, - pageSize: TABLE_DEFAULT_PAGE_SIZE, - initial, - }); + dispatch(toggleDetailedLogs(false)); } }; @@ -149,7 +119,6 @@ const Logs = (props) => { (async () => { setIsLoading(true); - dispatch(setLogsPage(TABLE_FIRST_PAGE)); dispatch(getFilteringStatus()); dispatch(getClients()); try { @@ -169,6 +138,7 @@ const Logs = (props) => { mediaQuery.removeEventListener('change', mediaQueryHandler); } catch (e1) { try { + // Safari 13.1 do not support mediaQuery.addEventListener('change', handler) mediaQuery.removeListener(mediaQueryHandler); } catch (e2) { console.error(e2); @@ -179,99 +149,53 @@ const Logs = (props) => { }; }, []); - const refreshLogs = async () => { - setIsLoading(true); - await Promise.all([ - dispatch(setLogsPage(TABLE_FIRST_PAGE)), - dispatch(refreshFilteredLogs()), - ]); - dispatch(addSuccessToast('query_log_updated')); - setIsLoading(false); - }; + const renderPage = () => <> + + + + + + + {processContent(detailedDataCurrent, buttonType)} + + ; - return ( - <> - {enabled && processingGetConfig && } - {enabled && !processingGetConfig && ( - <> - - - - - - - {processContent(detailedDataCurrent, buttonType)} - - - )} - {!enabled && !processingGetConfig && ( - - )} - - ); -}; - -Logs.propTypes = { - getLogs: PropTypes.func.isRequired, - queryLogs: PropTypes.object.isRequired, - dashboard: PropTypes.object.isRequired, - getFilteringStatus: PropTypes.func.isRequired, - filtering: PropTypes.object.isRequired, - setRules: PropTypes.func.isRequired, - addSuccessToast: PropTypes.func.isRequired, - setLogsPagination: PropTypes.func.isRequired, - setLogsPage: PropTypes.func.isRequired, - toggleDetailedLogs: PropTypes.func.isRequired, - dnsConfig: PropTypes.object.isRequired, + return <> + {enabled && processingGetConfig && } + {enabled && !processingGetConfig && renderPage()} + {!enabled && !processingGetConfig && } + ; }; export default Logs; diff --git a/client/src/components/Settings/Dhcp/StaticLeases/index.js b/client/src/components/Settings/Dhcp/StaticLeases/index.js index d170be40..8df4c324 100644 --- a/client/src/components/Settings/Dhcp/StaticLeases/index.js +++ b/client/src/components/Settings/Dhcp/StaticLeases/index.js @@ -77,12 +77,12 @@ const StaticLeases = ({ title={t('delete_table_action')} disabled={processingDeleting} onClick={() => handleDelete(ip, mac, hostname)} - > - - - - - ; + > + + + + + ; }, }, ]} diff --git a/client/src/components/Toasts/Toast.js b/client/src/components/Toasts/Toast.js index f498abd9..a4c58aad 100644 --- a/client/src/components/Toasts/Toast.js +++ b/client/src/components/Toasts/Toast.js @@ -1,71 +1,50 @@ -import React, { Component } from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Trans, withTranslation } from 'react-i18next'; -import { FAILURE_TOAST_TIMEOUT, SUCCESS_TOAST_TIMEOUT } from '../../helpers/constants'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { TOAST_TIMEOUTS } from '../../helpers/constants'; +import { removeToast } from '../../actions'; -class Toast extends Component { - state = { - timerId: null, +const Toast = ({ + id, + message, + type, +}) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const [timerId, setTimerId] = useState(null); + + const clearRemoveToastTimeout = () => clearTimeout(timerId); + const removeCurrentToast = () => dispatch(removeToast(id)); + const setRemoveToastTimeout = () => { + const timeout = TOAST_TIMEOUTS[type]; + const timerId = setTimeout(removeCurrentToast, timeout); + + setTimerId(timerId); }; - componentDidMount() { - this.setRemoveToastTimeout(); - } + useEffect(() => { + setRemoveToastTimeout(); + }, []); - shouldComponentUpdate() { - return false; - } - - clearRemoveToastTimeout = () => clearTimeout(this.state.timerId); - - setRemoveToastTimeout = () => { - const timeout = this.props.type === 'success' ? SUCCESS_TOAST_TIMEOUT : FAILURE_TOAST_TIMEOUT; - - const timerId = setTimeout(() => { - this.props.removeToast(this.props.id); - }, timeout); - - this.setState({ timerId }); - }; - - showMessage(t, type, message) { - if (type === 'notice') { - return ; - } - - return {message}; - } - - render() { - const { - type, id, t, message, - } = this.props; - - return ( -
-

- {this.showMessage(t, type, message)} -

- -
- ); - } -} + return
+

{t(message)}

+ +
; +}; Toast.propTypes = { - t: PropTypes.func.isRequired, id: PropTypes.string.isRequired, message: PropTypes.string.isRequired, type: PropTypes.string.isRequired, - removeToast: PropTypes.func.isRequired, }; -export default withTranslation()(Toast); +export default Toast; diff --git a/client/src/components/Toasts/index.js b/client/src/components/Toasts/index.js index d765b097..a4388318 100644 --- a/client/src/components/Toasts/index.js +++ b/client/src/components/Toasts/index.js @@ -1,41 +1,25 @@ -import { connect } from 'react-redux'; import React from 'react'; -import PropTypes from 'prop-types'; +import { useSelector, shallowEqual } from 'react-redux'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; -import * as actionCreators from '../../actions'; +import { TOAST_TRANSITION_TIMEOUT } from '../../helpers/constants'; import Toast from './Toast'; - import './Toast.css'; -const Toasts = (props) => ( - - {props.toasts.notices?.map((toast) => { - const { id } = toast; - return ( - - - - ); - })} - -); +const Toasts = () => { + const toasts = useSelector((state) => state.toasts, shallowEqual); -Toasts.propTypes = { - toasts: PropTypes.object, - removeToast: PropTypes.func, + return + {toasts.notices?.map((toast) => { + const { id } = toast; + return + + ; + })} + ; }; -const mapStateToProps = (state) => { - const { toasts } = state; - const props = { toasts }; - return props; -}; - -export default connect( - mapStateToProps, - actionCreators, -)(Toasts); +export default Toasts; diff --git a/client/src/components/ui/Card.css b/client/src/components/ui/Card.css index e5840f7f..cef0e71d 100644 --- a/client/src/components/ui/Card.css +++ b/client/src/components/ui/Card.css @@ -118,14 +118,14 @@ } } -.card .red { +.card .logs__cell--red { background-color: #fff4f2; } -.card .green { +.card .logs__cell--green { background-color: #f1faf3; } -.card .blue { +.card .logs__row--blue { background-color: #ecf7ff; } diff --git a/client/src/components/ui/Loading.css b/client/src/components/ui/Loading.css index 6839b336..e75953cc 100644 --- a/client/src/components/ui/Loading.css +++ b/client/src/components/ui/Loading.css @@ -13,8 +13,7 @@ z-index: 100; width: 100%; min-height: 100vh; - background-color: rgba(255, 255, 255, 0.6); - opacity: 0.8; + background-color: rgba(255, 255, 255, 0.48); } .loading:after { diff --git a/client/src/components/ui/Loading.js b/client/src/components/ui/Loading.js index 2d4bcd9f..0f0bb017 100644 --- a/client/src/components/ui/Loading.js +++ b/client/src/components/ui/Loading.js @@ -1,14 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import './Loading.css'; -const Loading = ({ className }) => ( -
-); +const Loading = ({ className, text }) => { + const { t } = useTranslation(); + return
{t(text)}
; +}; Loading.propTypes = { className: PropTypes.string, + text: PropTypes.string, }; export default Loading; diff --git a/client/src/components/ui/ReactTable.css b/client/src/components/ui/ReactTable.css index 4595212d..e8b1f4ca 100644 --- a/client/src/components/ui/ReactTable.css +++ b/client/src/components/ui/ReactTable.css @@ -13,18 +13,18 @@ overflow: visible; } -.rt-tr-group.red { +.rt-tr-group.logs__row--red { background-color: rgba(223, 56, 18, 0.05); } -.rt-tr-group.green { +.rt-tr-group.logs__row--green { background-color: rgba(103, 178, 121, 0.1); } -.rt-tr-group.blue { +.rt-tr-group.logs__row--blue { background-color: #e5effd; } -.rt-tr-group.yellow { +.rt-tr-group.logs__row--yellow { background-color: var(--yellow-pale); } diff --git a/client/src/components/ui/Tooltip.js b/client/src/components/ui/Tooltip.js index 284a53a3..9f34b3fe 100644 --- a/client/src/components/ui/Tooltip.js +++ b/client/src/components/ui/Tooltip.js @@ -34,33 +34,47 @@ const Tooltip = ({ delayShowValue = 0; } + const renderTooltip = ({ tooltipRef, getTooltipProps }) => ( +
+ {typeof content === 'string' ? t(content) : content} +
+ ); + + const renderTrigger = ({ getTriggerProps, triggerRef }) => ( + + {children} + + ); + + renderTooltip.propTypes = { + tooltipRef: propTypes.object, + getTooltipProps: propTypes.func, + }; + + renderTrigger.propTypes = { + triggerRef: propTypes.object, + getTriggerProps: propTypes.func, + }; + return ( ( -
- {typeof content === 'string' ? t(content) : content} -
- )} + tooltip={renderTooltip} > - {({ getTriggerProps, triggerRef }) => ( - - {children} - - )} + {renderTrigger}
); }; diff --git a/client/src/containers/Dashboard.js b/client/src/containers/Dashboard.js index 2b2dce78..3d665483 100644 --- a/client/src/containers/Dashboard.js +++ b/client/src/containers/Dashboard.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { toggleProtection, getClients } from '../actions'; import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats'; -import { toggleClientBlock, getAccessList } from '../actions/access'; +import { getAccessList } from '../actions/access'; import Dashboard from '../components/Dashboard'; const mapStateToProps = (state) => { @@ -16,7 +16,6 @@ const mapDispatchToProps = { getStats, getStatsConfig, setStatsConfig, - toggleClientBlock, getAccessList, }; diff --git a/client/src/containers/Logs.js b/client/src/containers/Logs.js deleted file mode 100644 index 92858cbf..00000000 --- a/client/src/containers/Logs.js +++ /dev/null @@ -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); diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 46290b9a..2c8de73e 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -307,9 +307,7 @@ export const DEFAULT_LOGS_FILTER = { export const DEFAULT_LANGUAGE = 'en'; -export const TABLE_DEFAULT_PAGE_SIZE = 25; - -export const TABLE_FIRST_PAGE = 0; +export const QUERY_LOGS_PAGE_LIMIT = 20; export const LEASES_TABLE_DEFAULT_PAGE_SIZE = 20; @@ -327,85 +325,93 @@ export const FILTERED_STATUS = { export const RESPONSE_FILTER = { ALL: { - query: 'all', - label: 'all_queries', + QUERY: 'all', + LABEL: 'all_queries', }, FILTERED: { - query: 'filtered', - label: 'filtered', + QUERY: 'filtered', + LABEL: 'filtered', }, PROCESSED: { - query: 'processed', - label: 'show_processed_responses', + QUERY: 'processed', + LABEL: 'show_processed_responses', }, BLOCKED: { - query: 'blocked', - label: 'show_blocked_responses', + QUERY: 'blocked', + LABEL: 'show_blocked_responses', }, BLOCKED_THREATS: { - query: 'blocked_safebrowsing', - label: 'blocked_threats', + QUERY: 'blocked_safebrowsing', + LABEL: 'blocked_threats', }, BLOCKED_ADULT_WEBSITES: { - query: 'blocked_parental', - label: 'blocked_adult_websites', + QUERY: 'blocked_parental', + LABEL: 'blocked_adult_websites', }, ALLOWED: { - query: 'whitelisted', - label: 'allowed', + QUERY: 'whitelisted', + LABEL: 'allowed', }, REWRITTEN: { - query: 'rewritten', - label: 'rewritten', + QUERY: 'rewritten', + LABEL: 'rewritten', }, SAFE_SEARCH: { - query: 'safe_search', - label: 'safe_search', + QUERY: 'safe_search', + LABEL: 'safe_search', }, }; export const RESPONSE_FILTER_QUERIES = Object.values(RESPONSE_FILTER) - .reduce((acc, { query }) => { - acc[query] = query; + .reduce((acc, { QUERY }) => { + acc[QUERY] = QUERY; return acc; }, {}); +export const QUERY_STATUS_COLORS = { + BLUE: 'blue', + GREEN: 'green', + RED: 'red', + WHITE: 'white', + YELLOW: 'yellow', +}; + export const FILTERED_STATUS_TO_META_MAP = { [FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: { - label: RESPONSE_FILTER.ALLOWED.label, - color: 'green', + LABEL: RESPONSE_FILTER.ALLOWED.LABEL, + COLOR: QUERY_STATUS_COLORS.GREEN, }, [FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: { - label: RESPONSE_FILTER.PROCESSED.label, - color: 'white', + LABEL: RESPONSE_FILTER.PROCESSED.LABEL, + COLOR: QUERY_STATUS_COLORS.WHITE, }, [FILTERED_STATUS.FILTERED_BLOCKED_SERVICE]: { - label: RESPONSE_FILTER.BLOCKED.label, - color: 'red', + LABEL: RESPONSE_FILTER.BLOCKED.LABEL, + COLOR: QUERY_STATUS_COLORS.RED, }, [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: { - label: RESPONSE_FILTER.SAFE_SEARCH.label, - color: 'yellow', + LABEL: RESPONSE_FILTER.SAFE_SEARCH.LABEL, + COLOR: QUERY_STATUS_COLORS.YELLOW, }, [FILTERED_STATUS.FILTERED_BLACK_LIST]: { - label: RESPONSE_FILTER.BLOCKED.label, - color: 'red', + LABEL: RESPONSE_FILTER.BLOCKED.LABEL, + COLOR: QUERY_STATUS_COLORS.RED, }, [FILTERED_STATUS.REWRITE]: { - label: RESPONSE_FILTER.REWRITTEN.label, - color: 'blue', + LABEL: RESPONSE_FILTER.REWRITTEN.LABEL, + COLOR: QUERY_STATUS_COLORS.BLUE, }, [FILTERED_STATUS.REWRITE_HOSTS]: { - label: RESPONSE_FILTER.REWRITTEN.label, - color: 'blue', + LABEL: RESPONSE_FILTER.REWRITTEN.LABEL, + COLOR: QUERY_STATUS_COLORS.BLUE, }, [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: { - label: RESPONSE_FILTER.BLOCKED_THREATS.label, - color: 'yellow', + LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL, + COLOR: QUERY_STATUS_COLORS.YELLOW, }, [FILTERED_STATUS.FILTERED_PARENTAL]: { - label: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.label, - color: 'yellow', + LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL, + COLOR: QUERY_STATUS_COLORS.YELLOW, }, }; @@ -519,3 +525,17 @@ export const DHCP_DESCRIPTION_PLACEHOLDERS = { lease_duration: 'dhcp_form_lease_input', }, }; + +export const TOAST_TRANSITION_TIMEOUT = 500; + +export const TOAST_TYPES = { + SUCCESS: 'success', + ERROR: 'error', + NOTICE: 'notice', +}; + +export const TOAST_TIMEOUTS = { + [TOAST_TYPES.SUCCESS]: 5000, + [TOAST_TYPES.ERROR]: 30000, + [TOAST_TYPES.NOTICE]: 30000, +}; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index baae34f5..9fdd9fad 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -15,6 +15,7 @@ import { getTrackerData } from './trackers/trackers'; import { CHECK_TIMEOUT, + CUSTOM_FILTERING_RULES_ID, DEFAULT_DATE_FORMAT_OPTIONS, DEFAULT_LANGUAGE, DEFAULT_TIME_FORMAT, @@ -742,6 +743,30 @@ export const sortIp = (a, b) => { } }; +/** + * @param {array} filters + * @param {array} whitelistFilters + * @param {number} filterId + * @param {function} t - translate + * @returns {string} + */ +export const getFilterName = ( + filters, + whitelistFilters, + filterId, + customFilterTranslationKey = 'custom_filter_rules', + resolveFilterName = (filter) => (filter ? filter.name : i18n.t('unknown_filter', { filterId })), +) => { + if (filterId === CUSTOM_FILTERING_RULES_ID) { + return i18n.t(customFilterTranslationKey); + } + + const matchIdPredicate = (filter) => filter.id === filterId; + const filter = filters.find(matchIdPredicate) || whitelistFilters.find(matchIdPredicate); + + return resolveFilterName(filter); +}; + /** * @param ip {string} * @param gateway_ip {string} @@ -803,3 +828,11 @@ export const enrichWithConcatenatedIpAddresses = (interfaces) => Object.entries( acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses); return acc; }, interfaces); + +export const isScrolledIntoView = (el) => { + const rect = el.getBoundingClientRect(); + const elemTop = rect.top; + const elemBottom = rect.bottom; + + return elemTop < window.innerHeight && elemBottom >= 0; +}; diff --git a/client/src/helpers/formatClientCell.js b/client/src/helpers/renderFormattedClientCell.js similarity index 51% rename from client/src/helpers/formatClientCell.js rename to client/src/helpers/renderFormattedClientCell.js index 48b89665..204d29dc 100644 --- a/client/src/helpers/formatClientCell.js +++ b/client/src/helpers/renderFormattedClientCell.js @@ -24,9 +24,17 @@ const getFormattedWhois = (whois) => { ); }; -export const formatClientCell = (row, isDetailed = false, isLogs = true) => { - const { value, original: { info } } = row; - let whoisContainer = ''; +/** + * @param {string} value + * @param {object} info + * @param {string} info.name + * @param {object} info.whois_info + * @param {boolean} [isDetailed] + * @param {boolean} [isLogs] + * @returns {JSX.Element} + */ +export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => { + let whoisContainer = null; let nameContainer = value; if (info) { @@ -34,42 +42,28 @@ export const formatClientCell = (row, isDetailed = false, isLogs = true) => { const whoisAvailable = whois_info && Object.keys(whois_info).length > 0; if (name) { - if (isLogs) { - nameContainer = !whoisAvailable && isDetailed - ? ( - {value} - ) : ( -
- {name} {`(${value})`} -
- ); + const nameValue =
+ {name} {`(${value})`} +
; + + if (!isLogs) { + nameContainer = nameValue; } else { - nameContainer = ( -
- {name} {`(${value})`} -
- ); + nameContainer = !whoisAvailable && isDetailed + ? {value} + : nameValue; } } if (whoisAvailable && isDetailed) { - whoisContainer = ( -
+ whoisContainer =
{getFormattedWhois(whois_info)} -
- ); +
; } } - return ( -
- <> - {nameContainer} - {whoisContainer} - -
- ); + return
+ {nameContainer} + {whoisContainer} +
; }; diff --git a/client/src/reducers/queryLogs.js b/client/src/reducers/queryLogs.js index a4a3d180..19d92afb 100644 --- a/client/src/reducers/queryLogs.js +++ b/client/src/reducers/queryLogs.js @@ -1,30 +1,10 @@ import { handleActions } from 'redux-actions'; import * as actions from '../actions/queryLogs'; -import { DEFAULT_LOGS_FILTER, TABLE_DEFAULT_PAGE_SIZE } from '../helpers/constants'; +import { DEFAULT_LOGS_FILTER } from '../helpers/constants'; const queryLogs = handleActions( { - [actions.setLogsPagination]: (state, { payload }) => { - const { page, pageSize } = payload; - const { allLogs } = state; - const rowsStart = pageSize * page; - const rowsEnd = (pageSize * page) + pageSize; - const logsSlice = allLogs.slice(rowsStart, rowsEnd); - const pages = Math.ceil(allLogs.length / pageSize); - - return { - ...state, - pages, - logs: logsSlice, - }; - }, - - [actions.setLogsPage]: (state, { payload }) => ({ - ...state, - page: payload, - }), - [actions.setFilteredLogsRequest]: (state) => ({ ...state, processingGetLogs: true }), [actions.setFilteredLogsFailure]: (state) => ({ ...state, processingGetLogs: false }), [actions.toggleDetailedLogs]: (state, { payload }) => ({ @@ -34,14 +14,7 @@ const queryLogs = handleActions( [actions.setFilteredLogsSuccess]: (state, { payload }) => { const { logs, oldest, filter } = payload; - const pageSize = TABLE_DEFAULT_PAGE_SIZE; - const page = 0; - const pages = Math.ceil(logs.length / pageSize); - const total = logs.length; - const rowsStart = pageSize * page; - const rowsEnd = rowsStart + pageSize; - const logsSlice = logs.slice(rowsStart, rowsEnd); const isFiltered = filter && Object.keys(filter).some((key) => filter[key]); return { @@ -49,10 +22,8 @@ const queryLogs = handleActions( oldest, filter, isFiltered, - pages, - total, - logs: logsSlice, - allLogs: logs, + logs, + isEntireLog: logs.length < 1, processingGetLogs: false, }; }, @@ -67,29 +38,13 @@ const queryLogs = handleActions( [actions.getLogsFailure]: (state) => ({ ...state, processingGetLogs: false }), [actions.getLogsSuccess]: (state, { payload }) => { const { - logs, oldest, older_than, page, pageSize, initial, + logs, oldest, older_than, } = payload; - let logsWithOffset = state.allLogs.length > 0 && !initial ? state.allLogs : logs; - let allLogs = logs; - - if (older_than) { - logsWithOffset = [...state.allLogs, ...logs]; - allLogs = [...state.allLogs, ...logs]; - } - - const pages = Math.ceil(logsWithOffset.length / pageSize); - const total = logsWithOffset.length; - const rowsStart = pageSize * page; - const rowsEnd = (pageSize * page) + pageSize; - const logsSlice = logsWithOffset.slice(rowsStart, rowsEnd); return { ...state, oldest, - pages, - total, - allLogs, - logs: logsSlice, + logs: older_than ? [...state.logs, ...logs] : logs, isEntireLog: logs.length < 1, processingGetLogs: false, }; @@ -126,7 +81,7 @@ const queryLogs = handleActions( ...state, processingAdditionalLogs: false, processingGetLogs: false, }), [actions.getAdditionalLogsSuccess]: (state) => ({ - ...state, processingAdditionalLogs: false, processingGetLogs: false, + ...state, processingAdditionalLogs: false, processingGetLogs: false, isEntireLog: true, }), }, { @@ -135,18 +90,15 @@ const queryLogs = handleActions( processingGetConfig: false, processingSetConfig: false, processingAdditionalLogs: false, - logs: [], interval: 1, - allLogs: [], - page: 0, - pages: 0, - total: 0, + logs: [], enabled: true, oldest: '', filter: DEFAULT_LOGS_FILTER, isFiltered: false, anonymize_client_ip: false, isDetailed: true, + isEntireLog: false, }, ); diff --git a/client/src/reducers/toasts.js b/client/src/reducers/toasts.js index 7a48c72d..be66bd57 100644 --- a/client/src/reducers/toasts.js +++ b/client/src/reducers/toasts.js @@ -5,16 +5,17 @@ import { addErrorToast, addNoticeToast, addSuccessToast, } from '../actions/toasts'; import { removeToast } from '../actions'; +import { TOAST_TYPES } from '../helpers/constants'; const toasts = handleActions({ [addErrorToast]: (state, { payload }) => { const message = payload.error.toString(); - console.error(message); + console.error(payload.error); const errorToast = { id: nanoid(), message, - type: 'error', + type: TOAST_TYPES.ERROR, }; const newState = { ...state, notices: [...state.notices, errorToast] }; @@ -24,7 +25,7 @@ const toasts = handleActions({ const successToast = { id: nanoid(), message: payload, - type: 'success', + type: TOAST_TYPES.SUCCESS, }; const newState = { ...state, notices: [...state.notices, successToast] }; @@ -34,7 +35,7 @@ const toasts = handleActions({ const noticeToast = { id: nanoid(), message: payload.error.toString(), - type: 'notice', + type: TOAST_TYPES.NOTICE, }; const newState = { ...state, notices: [...state.notices, noticeToast] }; diff --git a/client/webpack.common.js b/client/webpack.common.js index 69094123..0669ed9d 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -41,9 +41,8 @@ const config = { alias: { MainRoot: path.resolve(__dirname, '../'), ClientRoot: path.resolve(__dirname, './src'), - // TODO: change to '@hot-loader/react-dom' when v16.13.1 is released - // https://stackoverflow.com/a/62671689/12942752 - 'react-dom': 'react-dom', + // TODO: uncomment when v16.13.1 is released https://stackoverflow.com/a/62671689/12942752 + // 'react-dom': '@hot-loader/react-dom', }, }, module: { diff --git a/querylog/qlog_http.go b/querylog/qlog_http.go index cf804c47..93647a8d 100644 --- a/querylog/qlog_http.go +++ b/querylog/qlog_http.go @@ -165,13 +165,12 @@ func (l *queryLog) parseSearchParams(r *http.Request) (*searchParams, error) { if limit, err := strconv.ParseInt(q.Get("limit"), 10, 64); err == nil { p.limit = int(limit) - - // If limit or offset are specified explicitly, we should change the default behavior - // and scan all log records until we found enough log entries - p.maxFileScanEntries = 0 } if offset, err := strconv.ParseInt(q.Get("offset"), 10, 64); err == nil { p.offset = int(offset) + + // If we don't use "olderThan" and use offset/limit instead, we should change the default behavior + // and scan all log records until we found enough log entries p.maxFileScanEntries = 0 }