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ávod0> 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 instruktion0> 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 Anweisung0>, 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ón0> 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 instruction0> 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 upute0> 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 ini0> 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>この手順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 instructie0> 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ę0> 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țiune0> 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>инструкцией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ávod0> 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 navodilo0> 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 uputstvo0> 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ı0> 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>这份关于如何解决这一问题的说明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>用法說明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 =
@@ -157,7 +146,6 @@ Dashboard.propTypes = {
getStatsConfig: PropTypes.func.isRequired,
toggleProtection: PropTypes.func.isRequired,
getClients: PropTypes.func.isRequired,
- toggleClientBlock: PropTypes.func.isRequired,
getAccessList: PropTypes.func.isRequired,
};
diff --git a/client/src/components/Filters/Check/Info.js b/client/src/components/Filters/Check/Info.js
index 26e550c6..0c73c55a 100644
--- a/client/src/components/Filters/Check/Info.js
+++ b/client/src/components/Filters/Check/Info.js
@@ -11,25 +11,10 @@ import {
checkWhiteList,
checkSafeSearch,
checkSafeBrowsing,
- checkParental,
+ checkParental, getFilterName,
} from '../../../helpers/helpers';
import { FILTERED } from '../../../helpers/constants';
-const getFilterName = (id, filters, whitelistFilters, t) => {
- if (id === 0) {
- return t('filtered_custom_rules');
- }
-
- const filter = filters.find((filter) => filter.id === id)
- || whitelistFilters.find((filter) => filter.id === id);
-
- if (filter && filter.name) {
- return t('query_log_filtered', { filter: filter.name });
- }
-
- return '';
-};
-
const getTitle = (reason, filterName, t, onlyFiltered) => {
if (checkNotFilteredNotFound(reason)) {
return t('check_not_found');
@@ -101,7 +86,12 @@ const Info = ({
ip_addrs,
t,
}) => {
- const filterName = getFilterName(filter_id, filters, whitelistFilters, t);
+ const filterName = getFilterName(filters,
+ whitelistFilters,
+ filter_id,
+ 'filtered_custom_rules',
+ (filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''));
+
const onlyFiltered = checkSafeSearch(reason)
|| checkSafeBrowsing(reason)
|| checkParental(reason);
diff --git a/client/src/components/Filters/Table.js b/client/src/components/Filters/Table.js
index ed97ac0d..4eee8e9b 100644
--- a/client/src/components/Filters/Table.js
+++ b/client/src/components/Filters/Table.js
@@ -48,7 +48,7 @@ class Table extends Component {
accessor: 'url',
minWidth: 200,
Cell: ({ value }) => (
-
+
{isValidAbsolutePath(value) ? value
:
{
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
+ const processingRules = useSelector((state) => state.filtering.processingRules);
+ const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
+
+ const autoClient = autoClients.find((autoClient) => autoClient.name === client);
+ const source = autoClient?.source;
+ const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
+
+ const id = nanoid();
+
+ const data = {
+ address: client,
+ name,
+ country: whois_info?.country,
+ city: whois_info?.city,
+ network: whois_info?.orgname,
+ source_label: source,
+ };
+
+ const processedData = Object.entries(data);
+
+ const isFiltered = checkFiltered(reason);
+
+ const nameClass = classNames('w-90 o-hidden d-flex flex-column', {
+ 'mt-2': isDetailed && !name && !whoisAvailable,
+ 'white-space--nowrap': isDetailed,
+ });
+
+ const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
+ 'my-3': isDetailed,
+ });
+
+ const renderBlockingButton = (isFiltered, domain) => {
+ const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
+
+ const buttonClass = classNames('btn btn-sm logs__cell--block-button', {
+ 'btn-outline-secondary': isFiltered,
+ 'btn-outline-danger': !isFiltered,
+ });
+
+ const onClick = () => dispatch(toggleBlocking(buttonType, domain));
+
+ return ;
+ };
+
+ 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')}
+ {
+
+
+ }
+ >,
+ },
+ ];
+
+ 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')}
- {
-
-
- }
-
;
- },
- 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={
- }
- nextText={
- }
- 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
}