From a0013659de73ccb2ffff1b73deef3965fbdeb0bc Mon Sep 17 00:00:00 2001 From: maxim_tereshin Date: Thu, 2 Jul 2020 14:29:12 +0500 Subject: [PATCH] Add filtering and pagination for topic messages (#66) * Add filtering and pagination for topic messages * Add delay to search query, momoize some functions --- kafka-ui-react-app/.eslintrc.json | 1 + kafka-ui-react-app/package-lock.json | 137 +++++---- kafka-ui-react-app/package.json | 4 +- kafka-ui-react-app/src/components/App.scss | 4 + .../Topics/Details/Messages/Messages.tsx | 262 +++++++++++++----- .../Details/Messages/MessagesContainer.ts | 14 +- .../Form/CustomParams/CustomParamButton.tsx | 1 + .../Form/CustomParams/CustomParamField.tsx | 4 - .../Form/CustomParams/CustomParamValue.tsx | 1 - .../src/redux/actions/thunks.ts | 10 +- kafka-ui-react-app/src/redux/api/topics.ts | 28 +- .../src/redux/interfaces/topic.ts | 16 +- 12 files changed, 349 insertions(+), 133 deletions(-) diff --git a/kafka-ui-react-app/.eslintrc.json b/kafka-ui-react-app/.eslintrc.json index 6dc2d5b711c..a28472dca95 100644 --- a/kafka-ui-react-app/.eslintrc.json +++ b/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", diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index c4db16d76c1..19d0a8eee37 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/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", diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index b145959d691..7c7c7aeb49e 100644 --- a/kafka-ui-react-app/package.json +++ b/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", diff --git a/kafka-ui-react-app/src/components/App.scss b/kafka-ui-react-app/src/components/App.scss index a3b857b53c8..a6babfa0aeb 100644 --- a/kafka-ui-react-app/src/components/App.scss +++ b/kafka-ui-react-app/src/components/App.scss @@ -28,3 +28,7 @@ $navbar-width: 250px; overflow-y: scroll; } } + +.react-datepicker-wrapper { + display: flex !important; +} diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx b/kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx index 012952b15b6..f6603d9f44f 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx +++ b/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 + ) => 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 = ({ isFetched, clusterName, @@ -18,88 +49,189 @@ const Messages: React.FC = ({ messages, fetchTopicMessages, }) => { + const [searchQuery, setSearchQuery] = React.useState(''); + const [searchTimestamp, setSearchTimestamp] = React.useState( + null + ); + const [filterProps, setFilterProps] = React.useState([]); + const [queryParams, setQueryParams] = React.useState< + Partial + >({ 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) => { + const query = event.target.value; + + setSearchQuery(query); + handleDelayedQuery(query); + }; - const [searchText, setSearchText] = React.useState(''); + const handleDateTimeChange = () => { + if (searchTimestamp !== prevSearchTimestamp) { + if (searchTimestamp) { + const timestamp: number = searchTimestamp.getTime(); - const handleInputChange = (event: React.ChangeEvent) => { - 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({`content.${k}`}) - ); - + try { + const content = + typeof message.content !== 'object' + ? JSON.parse(message.content) + : message.content; + Object.keys(content).forEach((k) => + headers.push({`content.${k}`}) + ); + } catch (e) { + headers.push(Content); + } 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({JSON.stringify(v)})); + try { + const c = typeof content !== 'object' ? JSON.parse(content) : content; + Object.values(c).map((v) => + columns.push({JSON.stringify(v)}) + ); + } catch (e) { + columns.push({content}); + } return columns; }; - return ( - // eslint-disable-next-line no-nested-ternary - isFetched ? ( - messages.length > 0 ? ( -
-
-
- -
-
- - - - - - - {getMessageContentHeaders()} + const onNext = (event: React.MouseEvent) => { + event.preventDefault(); + + const seekTo: string[] = filterProps.map( + (p) => `${p.partition}::${p.offset}` + ); + setQueryParams({ + ...queryParams, + seekType: SeekTypes.OFFSET, + seekTo, + }); + }; + + const getTopicMessagesTable = () => { + return messages.length > 0 ? ( +
+
TimestampOffsetPartition
+ + + + + + {getMessageContentHeaders} + + + + {messages.map((message) => ( + + + + + {getMessageContentBody(message.content)} - - - {messages - .filter( - (message) => - !searchText || message?.content?.indexOf(searchText) >= 0 - ) - .map((message) => ( - - - - - {getMessageContentBody(message.content)} - - ))} - -
TimestampOffsetPartition
{getTimestampDate(message.timestamp)}{message.offset}{message.partition}
{getTimestampDate(message.timestamp)}{message.offset}{message.partition}
+ ))} + + +
+
+ +
- ) : ( -
No messages at selected topic
- ) +
) : ( - - ) +
No messages at selected topic
+ ); + }; + + return isFetched ? ( +
+
+
+ + setSearchTimestamp(date)} + onCalendarClose={handleDateTimeChange} + isClearable + showTimeInput + timeInputLabel="Time:" + dateFormat="MMMM d, yyyy h:mm aa" + className="input" + /> +
+
+ + +
+
+
{getTopicMessagesTable()}
+
+ ) : ( + ); }; diff --git a/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts b/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts index 336389054aa..23dae9d4a71 100644 --- a/kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts +++ b/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 + ) => fetchTopicMessages(clusterName, topicName, queryParams), }; export default withRouter( diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamButton.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamButton.tsx index ea5c731df52..0276beabc10 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamButton.tsx +++ b/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 { diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx index a0f79e8614e..b6256ead976 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx +++ b/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; diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamValue.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamValue.tsx index c04343d0b46..2584166fb0b 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamValue.tsx +++ b/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 { diff --git a/kafka-ui-react-app/src/redux/actions/thunks.ts b/kafka-ui-react-app/src/redux/actions/thunks.ts index d3134727280..4742296aafe 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks.ts +++ b/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 ): PromiseThunk => 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()); diff --git a/kafka-ui-react-app/src/redux/api/topics.ts b/kafka-ui-react-app/src/redux/api/topics.ts index e12473f1d58..55d3cc2dbbb 100644 --- a/kafka-ui-react-app/src/redux/api/topics.ts +++ b/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 => export const getTopicMessages = ( clusterName: ClusterName, - topicName: TopicName -): Promise => - fetch(`${BASE_URL}/clusters/${clusterName}/topics/${topicName}/messages`, { - ...BASE_PARAMS, - }).then((res) => res.json()); + topicName: TopicName, + queryParams: Partial +): Promise => { + 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, diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts index 1d56ebec550..1c98bb20e97 100644 --- a/kafka-ui-react-app/src/redux/interfaces/topic.ts +++ b/kafka-ui-react-app/src/redux/interfaces/topic.ts @@ -57,7 +57,21 @@ export interface TopicMessage { timestampType: string; key: string; headers: Record; - 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 {