Browse Source

Add filtering and pagination for topic messages (#66)

* Add filtering and pagination for topic messages

* Add delay to search query, momoize some functions
maxim_tereshin 5 năm trước cách đây
mục cha
commit
f71c601d0d

+ 1 - 0
kafka-ui-react-app/.eslintrc.json

@@ -24,6 +24,7 @@
     "plugin:@typescript-eslint/recommended"
   ],
   "rules": {
+    "@typescript-eslint/ban-ts-ignore": "off",
     "import/extensions": [
       "error",
       "ignorePackages",

+ 86 - 51
kafka-ui-react-app/package-lock.json

@@ -1916,8 +1916,7 @@
     "@types/prop-types": {
       "version": "15.7.3",
       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
-      "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
-      "dev": true
+      "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
     },
     "@types/q": {
       "version": "1.5.4",
@@ -1929,12 +1928,21 @@
       "version": "16.9.17",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.17.tgz",
       "integrity": "sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg==",
-      "dev": true,
       "requires": {
         "@types/prop-types": "*",
         "csstype": "^2.2.0"
       }
     },
+    "@types/react-datepicker": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-3.0.2.tgz",
+      "integrity": "sha512-xW04NZRF+9ZnzOD3XrlIzBEKgUsN6LVgZJJsXH8NIUlVjyPh+sdtLPfVoDp+GQzGq1M0TuMLNZsv0sJ3N9XwDA==",
+      "requires": {
+        "@types/react": "*",
+        "date-fns": "^2.0.1",
+        "popper.js": "^1.14.1"
+      }
+    },
     "@types/react-dom": {
       "version": "16.9.4",
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.4.tgz",
@@ -3369,7 +3377,8 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
           "dev": true
         }
       }
@@ -4717,6 +4726,15 @@
         "sha.js": "^2.4.8"
       }
     },
+    "create-react-context": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz",
+      "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==",
+      "requires": {
+        "gud": "^1.0.0",
+        "warning": "^4.0.3"
+      }
+    },
     "cross-spawn": {
       "version": "6.0.5",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -5040,8 +5058,7 @@
     "csstype": {
       "version": "2.6.8",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz",
-      "integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA==",
-      "dev": true
+      "integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA=="
     },
     "currently-unhandled": {
       "version": "0.4.1",
@@ -5152,7 +5169,6 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
       "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
-      "dev": true,
       "requires": {
         "is-arguments": "^1.0.4",
         "is-date-object": "^1.0.1",
@@ -5203,7 +5219,6 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
       "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
-      "dev": true,
       "requires": {
         "object-keys": "^1.0.12"
       }
@@ -5787,7 +5802,6 @@
       "version": "1.17.0-next.1",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0-next.1.tgz",
       "integrity": "sha512-7MmGr03N7Rnuid6+wyhD9sHNE2n4tFSwExnU2lQl3lIo2ShXWGePY80zYaoMOmILWv57H0amMjZGHNzzGG70Rw==",
-      "dev": true,
       "requires": {
         "es-to-primitive": "^1.2.1",
         "function-bind": "^1.1.1",
@@ -5806,7 +5820,6 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
       "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "dev": true,
       "requires": {
         "is-callable": "^1.1.4",
         "is-date-object": "^1.0.1",
@@ -6917,7 +6930,8 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
           "dev": true
         }
       }
@@ -7505,8 +7519,7 @@
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
     "functional-red-black-tree": {
       "version": "1.0.1",
@@ -7839,7 +7852,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
       "requires": {
         "function-bind": "^1.1.1"
       }
@@ -7870,8 +7882,7 @@
     "has-symbols": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
-      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
-      "dev": true
+      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
     },
     "has-unicode": {
       "version": "2.0.1",
@@ -8709,8 +8720,7 @@
     "is-arguments": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
-      "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
-      "dev": true
+      "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA=="
     },
     "is-arrayish": {
       "version": "0.2.1",
@@ -8737,8 +8747,7 @@
     "is-callable": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
-      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
-      "dev": true
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA=="
     },
     "is-ci": {
       "version": "2.0.0",
@@ -8775,8 +8784,7 @@
     "is-date-object": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
-      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
-      "dev": true
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
     },
     "is-descriptor": {
       "version": "0.1.6",
@@ -8942,7 +8950,6 @@
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
       "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
-      "dev": true,
       "requires": {
         "has": "^1.0.3"
       }
@@ -8990,7 +8997,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
       "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
-      "dev": true,
       "requires": {
         "has-symbols": "^1.0.1"
       }
@@ -11864,14 +11870,12 @@
     "object-inspect": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
-      "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
-      "dev": true
+      "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw=="
     },
     "object-is": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz",
       "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
@@ -11881,7 +11885,6 @@
           "version": "1.17.5",
           "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
           "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
-          "dev": true,
           "requires": {
             "es-to-primitive": "^1.2.1",
             "function-bind": "^1.1.1",
@@ -11899,14 +11902,12 @@
         "is-callable": {
           "version": "1.1.5",
           "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
-          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
-          "dev": true
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
         },
         "string.prototype.trimleft": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
           "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
-          "dev": true,
           "requires": {
             "define-properties": "^1.1.3",
             "es-abstract": "^1.17.5",
@@ -11917,7 +11918,6 @@
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
           "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
-          "dev": true,
           "requires": {
             "define-properties": "^1.1.3",
             "es-abstract": "^1.17.5",
@@ -11929,8 +11929,7 @@
     "object-keys": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
     },
     "object-path": {
       "version": "0.11.4",
@@ -11951,7 +11950,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
       "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.2",
         "function-bind": "^1.1.1",
@@ -12595,6 +12593,11 @@
         "ts-pnp": "^1.1.2"
       }
     },
+    "popper.js": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+      "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
+    },
     "portfinder": {
       "version": "1.0.26",
       "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
@@ -14021,6 +14024,18 @@
         "whatwg-fetch": "^3.0.0"
       }
     },
+    "react-datepicker": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-3.0.0.tgz",
+      "integrity": "sha512-Yrxan1tERAiWS0EzitpiaiXOIz0APTUtV75uWbaS+jSaKoGCR6wUN2FDwr1ACGlnEoGhR9QQ2Vq3odnWtgJsOA==",
+      "requires": {
+        "classnames": "^2.2.6",
+        "date-fns": "^2.0.1",
+        "prop-types": "^15.7.2",
+        "react-onclickoutside": "^6.9.0",
+        "react-popper": "^1.3.4"
+      }
+    },
     "react-dev-utils": {
       "version": "10.2.1",
       "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",
@@ -14298,6 +14313,25 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
       "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
     },
+    "react-onclickoutside": {
+      "version": "6.9.0",
+      "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz",
+      "integrity": "sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A=="
+    },
+    "react-popper": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
+      "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
+      "requires": {
+        "@babel/runtime": "^7.1.2",
+        "create-react-context": "^0.3.0",
+        "deep-equal": "^1.1.1",
+        "popper.js": "^1.14.4",
+        "prop-types": "^15.6.1",
+        "typed-styles": "^0.0.7",
+        "warning": "^4.0.2"
+      }
+    },
     "react-redux": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz",
@@ -14817,7 +14851,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
       "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.0-next.1"
@@ -16156,7 +16189,8 @@
         },
         "kind-of": {
           "version": "6.0.2",
-          "resolved": "",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
           "dev": true
         }
       }
@@ -16679,7 +16713,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
       "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
@@ -16689,7 +16722,6 @@
           "version": "1.17.5",
           "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
           "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
-          "dev": true,
           "requires": {
             "es-to-primitive": "^1.2.1",
             "function-bind": "^1.1.1",
@@ -16707,14 +16739,12 @@
         "is-callable": {
           "version": "1.1.5",
           "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
-          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
-          "dev": true
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
         },
         "string.prototype.trimleft": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
           "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
-          "dev": true,
           "requires": {
             "define-properties": "^1.1.3",
             "es-abstract": "^1.17.5",
@@ -16725,7 +16755,6 @@
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
           "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
-          "dev": true,
           "requires": {
             "define-properties": "^1.1.3",
             "es-abstract": "^1.17.5",
@@ -16738,7 +16767,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz",
       "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "function-bind": "^1.1.1"
@@ -16748,7 +16776,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz",
       "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "function-bind": "^1.1.1"
@@ -16758,7 +16785,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
       "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
@@ -16768,7 +16794,6 @@
           "version": "1.17.5",
           "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
           "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
-          "dev": true,
           "requires": {
             "es-to-primitive": "^1.2.1",
             "function-bind": "^1.1.1",
@@ -16786,14 +16811,12 @@
         "is-callable": {
           "version": "1.1.5",
           "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
-          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
-          "dev": true
+          "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
         },
         "string.prototype.trimleft": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
           "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
-          "dev": true,
           "requires": {
             "define-properties": "^1.1.3",
             "es-abstract": "^1.17.5",
@@ -16804,7 +16827,6 @@
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
           "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
-          "dev": true,
           "requires": {
             "define-properties": "^1.1.3",
             "es-abstract": "^1.17.5",
@@ -17522,6 +17544,11 @@
         "mime-types": "~2.1.24"
       }
     },
+    "typed-styles": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz",
+      "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
+    },
     "typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@@ -17966,6 +17993,14 @@
         "makeerror": "1.0.x"
       }
     },
+    "warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "requires": {
+        "loose-envify": "^1.0.0"
+      }
+    },
     "watchpack": {
       "version": "1.7.2",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz",

+ 3 - 1
kafka-ui-react-app/package.json

@@ -3,14 +3,16 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@types/react-datepicker": "^3.0.2",
     "bulma": "^0.8.0",
     "bulma-switch": "^2.0.0",
     "classnames": "^2.2.6",
-    "immer": "^6.0.5",
     "date-fns": "^2.14.0",
+    "immer": "^6.0.5",
     "lodash": "^4.17.15",
     "pretty-ms": "^6.0.1",
     "react": "^16.12.0",
+    "react-datepicker": "^3.0.0",
     "react-dom": "^16.12.0",
     "react-hook-form": "^4.5.5",
     "react-redux": "^7.1.3",

+ 4 - 0
kafka-ui-react-app/src/components/App.scss

@@ -28,3 +28,7 @@ $navbar-width: 250px;
     overflow-y: scroll;
   }
 }
+
+.react-datepicker-wrapper {
+  display: flex !important;
+}

+ 197 - 65
kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx

@@ -1,16 +1,47 @@
-import React from 'react';
-import { ClusterName, TopicMessage, TopicName } from 'redux/interfaces';
+import React, { useCallback, useEffect, useRef } from 'react';
+import {
+  ClusterName,
+  SeekTypes,
+  TopicMessage,
+  TopicMessageQueryParams,
+  TopicName,
+} from 'redux/interfaces';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { format } from 'date-fns';
+import DatePicker from 'react-datepicker';
+
+import 'react-datepicker/dist/react-datepicker.css';
+import CustomParamButton, {
+  CustomParamButtonType,
+} from 'components/Topics/shared/Form/CustomParams/CustomParamButton';
+
+import { debounce } from 'lodash';
 
 interface Props {
   clusterName: ClusterName;
   topicName: TopicName;
   isFetched: boolean;
-  fetchTopicMessages: (clusterName: ClusterName, topicName: TopicName) => void;
+  fetchTopicMessages: (
+    clusterName: ClusterName,
+    topicName: TopicName,
+    queryParams: Partial<TopicMessageQueryParams>
+  ) => void;
   messages: TopicMessage[];
 }
 
+interface FilterProps {
+  offset: number;
+  partition: number;
+}
+
+function usePrevious(value: any) {
+  const ref = useRef();
+  useEffect(() => {
+    ref.current = value;
+  });
+  return ref.current;
+}
+
 const Messages: React.FC<Props> = ({
   isFetched,
   clusterName,
@@ -18,88 +49,189 @@ const Messages: React.FC<Props> = ({
   messages,
   fetchTopicMessages,
 }) => {
+  const [searchQuery, setSearchQuery] = React.useState<string>('');
+  const [searchTimestamp, setSearchTimestamp] = React.useState<Date | null>(
+    null
+  );
+  const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]);
+  const [queryParams, setQueryParams] = React.useState<
+    Partial<TopicMessageQueryParams>
+  >({ limit: 100 });
+
+  const prevSearchTimestamp = usePrevious(searchTimestamp);
+
+  const getUniqueDataForEachPartition: FilterProps[] = React.useMemo(() => {
+    const map = messages.map((message) => [
+      message.partition,
+      {
+        partition: message.partition,
+        offset: message.offset,
+      },
+    ]);
+    // @ts-ignore
+    return [...new Map(map).values()];
+  }, [messages]);
+
+  React.useEffect(() => {
+    fetchTopicMessages(clusterName, topicName, queryParams);
+  }, [fetchTopicMessages, clusterName, topicName, queryParams]);
+
   React.useEffect(() => {
-    fetchTopicMessages(clusterName, topicName);
-  }, [fetchTopicMessages, clusterName, topicName]);
+    setFilterProps(getUniqueDataForEachPartition);
+  }, [messages]);
+
+  const handleDelayedQuery = useCallback(
+    debounce(
+      (query: string) => setQueryParams({ ...queryParams, q: query }),
+      1000
+    ),
+    []
+  );
+  const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const query = event.target.value;
+
+    setSearchQuery(query);
+    handleDelayedQuery(query);
+  };
 
-  const [searchText, setSearchText] = React.useState<string>('');
+  const handleDateTimeChange = () => {
+    if (searchTimestamp !== prevSearchTimestamp) {
+      if (searchTimestamp) {
+        const timestamp: number = searchTimestamp.getTime();
 
-  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    setSearchText(event.target.value);
+        setSearchTimestamp(searchTimestamp);
+        setQueryParams({
+          ...queryParams,
+          seekType: SeekTypes.TIMESTAMP,
+          seekTo: filterProps.map((p) => `${p.partition}::${timestamp}`),
+        });
+      } else {
+        setSearchTimestamp(null);
+        const { seekTo, seekType, ...queryParamsWithoutSeek } = queryParams;
+        setQueryParams(queryParamsWithoutSeek);
+      }
+    }
   };
 
   const getTimestampDate = (timestamp: number) => {
     return format(new Date(timestamp * 1000), 'MM.dd.yyyy HH:mm:ss');
   };
 
-  const getMessageContentHeaders = () => {
+  const getMessageContentHeaders = React.useMemo(() => {
     const message = messages[0];
     const headers: JSX.Element[] = [];
-    const content = JSON.parse(message.content);
-    Object.keys(content).forEach((k) =>
-      headers.push(<th>{`content.${k}`}</th>)
-    );
-
+    try {
+      const content =
+        typeof message.content !== 'object'
+          ? JSON.parse(message.content)
+          : message.content;
+      Object.keys(content).forEach((k) =>
+        headers.push(<th key={Math.random()}>{`content.${k}`}</th>)
+      );
+    } catch (e) {
+      headers.push(<th>Content</th>);
+    }
     return headers;
-  };
+  }, [messages]);
 
-  const getMessageContentBody = (content: string) => {
-    const c = JSON.parse(content);
+  const getMessageContentBody = (content: any) => {
     const columns: JSX.Element[] = [];
-    Object.values(c).map((v) => columns.push(<td>{JSON.stringify(v)}</td>));
+    try {
+      const c = typeof content !== 'object' ? JSON.parse(content) : content;
+      Object.values(c).map((v) =>
+        columns.push(<td key={Math.random()}>{JSON.stringify(v)}</td>)
+      );
+    } catch (e) {
+      columns.push(<td>{content}</td>);
+    }
     return columns;
   };
 
-  return (
-    // eslint-disable-next-line no-nested-ternary
-    isFetched ? (
-      messages.length > 0 ? (
-        <div>
-          <div className="columns">
-            <div className="column is-half is-offset-half">
-              <input
-                id="searchText"
-                type="text"
-                name="searchText"
-                className="input"
-                placeholder="Search"
-                value={searchText}
-                onChange={handleInputChange}
-              />
-            </div>
-          </div>
-          <table className="table is-striped is-fullwidth">
-            <thead>
-              <tr>
-                <th>Timestamp</th>
-                <th>Offset</th>
-                <th>Partition</th>
-                {getMessageContentHeaders()}
+  const onNext = (event: React.MouseEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+
+    const seekTo: string[] = filterProps.map(
+      (p) => `${p.partition}::${p.offset}`
+    );
+    setQueryParams({
+      ...queryParams,
+      seekType: SeekTypes.OFFSET,
+      seekTo,
+    });
+  };
+
+  const getTopicMessagesTable = () => {
+    return messages.length > 0 ? (
+      <div>
+        <table className="table is-striped is-fullwidth">
+          <thead>
+            <tr>
+              <th>Timestamp</th>
+              <th>Offset</th>
+              <th>Partition</th>
+              {getMessageContentHeaders}
+            </tr>
+          </thead>
+          <tbody>
+            {messages.map((message) => (
+              <tr key={`${message.timestamp}${Math.random()}`}>
+                <td>{getTimestampDate(message.timestamp)}</td>
+                <td>{message.offset}</td>
+                <td>{message.partition}</td>
+                {getMessageContentBody(message.content)}
               </tr>
-            </thead>
-            <tbody>
-              {messages
-                .filter(
-                  (message) =>
-                    !searchText || message?.content?.indexOf(searchText) >= 0
-                )
-                .map((message) => (
-                  <tr key={message.timestamp}>
-                    <td>{getTimestampDate(message.timestamp)}</td>
-                    <td>{message.offset}</td>
-                    <td>{message.partition}</td>
-                    {getMessageContentBody(message.content)}
-                  </tr>
-                ))}
-            </tbody>
-          </table>
+            ))}
+          </tbody>
+        </table>
+        <div className="columns">
+          <div className="column is-full">
+            <CustomParamButton
+              className="is-link is-pulled-right"
+              type={CustomParamButtonType.chevronRight}
+              onClick={onNext}
+              btnText="Next"
+            />
+          </div>
         </div>
-      ) : (
-        <div>No messages at selected topic</div>
-      )
+      </div>
     ) : (
-      <PageLoader isFullHeight={false} />
-    )
+      <div>No messages at selected topic</div>
+    );
+  };
+
+  return isFetched ? (
+    <div>
+      <div className="columns">
+        <div className="column is-one-quarter">
+          <label className="label">Timestamp</label>
+          <DatePicker
+            selected={searchTimestamp}
+            onChange={(date) => setSearchTimestamp(date)}
+            onCalendarClose={handleDateTimeChange}
+            isClearable
+            showTimeInput
+            timeInputLabel="Time:"
+            dateFormat="MMMM d, yyyy h:mm aa"
+            className="input"
+          />
+        </div>
+        <div className="column is-two-quarters is-offset-one-quarter">
+          <label className="label">Search</label>
+          <input
+            id="searchText"
+            type="text"
+            name="searchText"
+            className="input"
+            placeholder="Search"
+            value={searchQuery}
+            onChange={handleQueryChange}
+          />
+        </div>
+      </div>
+      <div>{getTopicMessagesTable()}</div>
+    </div>
+  ) : (
+    <PageLoader isFullHeight={false} />
   );
 };
 

+ 11 - 3
kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts

@@ -1,5 +1,10 @@
 import { connect } from 'react-redux';
-import { ClusterName, RootState, TopicName } from 'redux/interfaces';
+import {
+  ClusterName,
+  RootState,
+  TopicMessageQueryParams,
+  TopicName,
+} from 'redux/interfaces';
 import { RouteComponentProps, withRouter } from 'react-router-dom';
 import { fetchTopicMessages } from 'redux/actions';
 import {
@@ -31,8 +36,11 @@ const mapStateToProps = (
 });
 
 const mapDispatchToProps = {
-  fetchTopicMessages: (clusterName: ClusterName, topicName: TopicName) =>
-    fetchTopicMessages(clusterName, topicName),
+  fetchTopicMessages: (
+    clusterName: ClusterName,
+    topicName: TopicName,
+    queryParams: Partial<TopicMessageQueryParams>
+  ) => fetchTopicMessages(clusterName, topicName, queryParams),
 };
 
 export default withRouter(

+ 1 - 0
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamButton.tsx

@@ -3,6 +3,7 @@ import React from 'react';
 export enum CustomParamButtonType {
   plus = 'fa-plus',
   minus = 'fa-minus',
+  chevronRight = 'fa-chevron-right',
 }
 
 interface Props {

+ 0 - 4
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx

@@ -1,11 +1,7 @@
 import React from 'react';
-import { useFormContext, ErrorMessage } from 'react-hook-form';
-import { TopicFormCustomParam } from 'redux/interfaces';
 import CustomParamSelect from 'components/Topics/shared/Form/CustomParams/CustomParamSelect';
 import CustomParamValue from 'components/Topics/shared/Form/CustomParams/CustomParamValue';
 import CustomParamAction from 'components/Topics/shared/Form/CustomParams/CustomParamAction';
-import { INDEX_PREFIX } from './CustomParams';
-import CustomParamOptions from './CustomParamOptions';
 
 interface Props {
   isDisabled: boolean;

+ 0 - 1
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamValue.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import { useFormContext, ErrorMessage } from 'react-hook-form';
-import { camelCase } from 'lodash';
 import CUSTOM_PARAMS_OPTIONS from './customParamsOptions';
 
 interface Props {

+ 8 - 2
kafka-ui-react-app/src/redux/actions/thunks.ts

@@ -7,6 +7,7 @@ import {
   TopicFormData,
   TopicName,
   Topic,
+  TopicMessageQueryParams,
 } from 'redux/interfaces';
 
 import * as actions from './actions';
@@ -59,11 +60,16 @@ export const fetchTopicList = (
 
 export const fetchTopicMessages = (
   clusterName: ClusterName,
-  topicName: TopicName
+  topicName: TopicName,
+  queryParams: Partial<TopicMessageQueryParams>
 ): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchTopicMessagesAction.request());
   try {
-    const messages = await api.getTopicMessages(clusterName, topicName);
+    const messages = await api.getTopicMessages(
+      clusterName,
+      topicName,
+      queryParams
+    );
     dispatch(actions.fetchTopicMessagesAction.success(messages));
   } catch (e) {
     dispatch(actions.fetchTopicMessagesAction.failure());

+ 23 - 5
kafka-ui-react-app/src/redux/api/topics.ts

@@ -9,6 +9,7 @@ import {
   TopicFormCustomParam,
   TopicFormFormattedParams,
   TopicFormCustomParams,
+  TopicMessageQueryParams,
 } from 'redux/interfaces';
 import { BASE_URL, BASE_PARAMS } from 'lib/constants';
 
@@ -49,11 +50,28 @@ export const getTopics = (clusterName: ClusterName): Promise<Topic[]> =>
 
 export const getTopicMessages = (
   clusterName: ClusterName,
-  topicName: TopicName
-): Promise<TopicMessage[]> =>
-  fetch(`${BASE_URL}/clusters/${clusterName}/topics/${topicName}/messages`, {
-    ...BASE_PARAMS,
-  }).then((res) => res.json());
+  topicName: TopicName,
+  queryParams: Partial<TopicMessageQueryParams>
+): Promise<TopicMessage[]> => {
+  let searchParams = '';
+  Object.entries({ ...queryParams }).forEach((entry) => {
+    const key = entry[0];
+    const value = entry[1];
+    if (value) {
+      if (Array.isArray(value)) {
+        searchParams += value.map((v) => `${key}=${v}&`);
+      } else {
+        searchParams += `${key}=${value}&`;
+      }
+    }
+  });
+  return fetch(
+    `${BASE_URL}/clusters/${clusterName}/topics/${topicName}/messages?${searchParams}`,
+    {
+      ...BASE_PARAMS,
+    }
+  ).then((res) => res.json());
+};
 
 export const postTopic = (
   clusterName: ClusterName,

+ 15 - 1
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -57,7 +57,21 @@ export interface TopicMessage {
   timestampType: string;
   key: string;
   headers: Record<string, string>;
-  content: string;
+  content: any;
+}
+
+export enum SeekTypes {
+  OFFSET = 'OFFSET',
+  TIMESTAMP = 'TIMESTAMP',
+}
+
+export type SeekType = keyof typeof SeekTypes;
+
+export interface TopicMessageQueryParams {
+  q: string;
+  limit: number;
+  seekType: SeekType;
+  seekTo: string[];
 }
 
 export interface TopicFormCustomParam {