瀏覽代碼

Topic messages filtering paginating (#72)

* Add filtering and pagination for topic messages

* Add delay to search query, momoize some functions

* Add partition selection
maxim_tereshin 5 年之前
父節點
當前提交
be2d38133d

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

@@ -57,7 +57,8 @@
       "node": {
       "node": {
         "extensions": [".js", ".jsx", ".ts", ".tsx"],
         "extensions": [".js", ".jsx", ".ts", ".tsx"],
         "paths": ["src"]
         "paths": ["src"]
-      }
+      },
+      "typescript": {}
     }
     }
   }
   }
 }
 }

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

@@ -1556,6 +1556,11 @@
       "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
       "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
       "dev": true
       "dev": true
     },
     },
+    "@rooks/use-outside-click": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/@rooks/use-outside-click/-/use-outside-click-3.6.0.tgz",
+      "integrity": "sha512-DDxdcD9bDDArV2tBmh5okaJNee/7EWaC5DsPrjTxIhhvXPpUatizcn2qYLcvX7y1vYpd64Wyqvkb87E6fsIfEQ=="
+    },
     "@samverschueren/stream-to-observable": {
     "@samverschueren/stream-to-observable": {
       "version": "0.3.0",
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
       "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
@@ -1889,6 +1894,11 @@
       "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
       "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
       "dev": true
       "dev": true
     },
     },
+    "@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
+    },
     "@types/lodash": {
     "@types/lodash": {
       "version": "4.14.149",
       "version": "4.14.149",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
@@ -5133,7 +5143,6 @@
       "version": "4.1.1",
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
       "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
       "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
-      "dev": true,
       "requires": {
       "requires": {
         "ms": "^2.1.1"
         "ms": "^2.1.1"
       }
       }
@@ -6054,6 +6063,18 @@
         }
         }
       }
       }
     },
     },
+    "eslint-import-resolver-typescript": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.0.0.tgz",
+      "integrity": "sha512-bT5Frpl8UWoHBtY25vKUOMoVIMlJQOMefHLyQ4Tz3MQpIZ2N6yYKEEIHMo38bszBNUuMBW6M3+5JNYxeiGFH4w==",
+      "requires": {
+        "debug": "^4.1.1",
+        "is-glob": "^4.0.1",
+        "resolve": "^1.12.0",
+        "tiny-glob": "^0.2.6",
+        "tsconfig-paths": "^3.9.0"
+      }
+    },
     "eslint-loader": {
     "eslint-loader": {
       "version": "3.0.3",
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-3.0.3.tgz",
       "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-3.0.3.tgz",
@@ -7726,6 +7747,11 @@
       "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
       "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
       "dev": true
       "dev": true
     },
     },
+    "globalyzer": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.4.tgz",
+      "integrity": "sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA=="
+    },
     "globby": {
     "globby": {
       "version": "8.0.2",
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz",
       "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz",
@@ -7755,6 +7781,11 @@
         }
         }
       }
       }
     },
     },
+    "globrex": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+      "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="
+    },
     "globule": {
     "globule": {
       "version": "1.3.0",
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz",
       "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz",
@@ -7766,6 +7797,11 @@
         "minimatch": "~3.0.2"
         "minimatch": "~3.0.2"
       }
       }
     },
     },
+    "goober": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/goober/-/goober-1.8.0.tgz",
+      "integrity": "sha512-9ZFoOkBccexjqIgcwlhq7C/eCSkgTZX0BdNUkOnBFLedrJgo3R8lp9ckd/qqtngtF/JDyXSxJzwMU98kNjZ4Mw=="
+    },
     "got": {
     "got": {
       "version": "9.6.0",
       "version": "9.6.0",
       "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
       "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
@@ -8826,8 +8862,7 @@
     "is-extglob": {
     "is-extglob": {
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "dev": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     },
     "is-finite": {
     "is-finite": {
       "version": "1.0.2",
       "version": "1.0.2",
@@ -8854,7 +8889,6 @@
       "version": "4.0.1",
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
       "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
       "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
-      "dev": true,
       "requires": {
       "requires": {
         "is-extglob": "^2.1.1"
         "is-extglob": "^2.1.1"
       }
       }
@@ -11213,8 +11247,7 @@
     "minimist": {
     "minimist": {
       "version": "1.2.5",
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-      "dev": true
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
     },
     "minipass": {
     "minipass": {
       "version": "3.1.3",
       "version": "3.1.3",
@@ -11365,8 +11398,7 @@
     "ms": {
     "ms": {
       "version": "2.1.2",
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     },
     "multicast-dns": {
     "multicast-dns": {
       "version": "6.2.3",
       "version": "6.2.3",
@@ -12427,8 +12459,7 @@
     "path-parse": {
     "path-parse": {
       "version": "1.0.6",
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
-      "dev": true
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
     },
     },
     "path-to-regexp": {
     "path-to-regexp": {
       "version": "0.1.7",
       "version": "0.1.7",
@@ -14313,6 +14344,15 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
       "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
       "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
     },
     },
+    "react-multi-select-component": {
+      "version": "2.0.12",
+      "resolved": "https://registry.npmjs.org/react-multi-select-component/-/react-multi-select-component-2.0.12.tgz",
+      "integrity": "sha512-QcOc8zQgz9AQQkX51EuDokqPi8BIRGBQdvnn1im3d1gsSIIY2W09jkvd9+/ByVk6NiL4XjygJtwCGJSGQcr3+A==",
+      "requires": {
+        "@rooks/use-outside-click": "^3.6.0",
+        "goober": "^1.8.0"
+      }
+    },
     "react-onclickoutside": {
     "react-onclickoutside": {
       "version": "6.9.0",
       "version": "6.9.0",
       "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz",
       "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz",
@@ -15102,7 +15142,6 @@
       "version": "1.12.2",
       "version": "1.12.2",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.2.tgz",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.2.tgz",
       "integrity": "sha512-cAVTI2VLHWYsGOirfeYVVQ7ZDejtQ9fp4YhYckWDEkFfqbVjaT11iM8k6xSAfGFMM+gDpZjMnFssPu8we+mqFw==",
       "integrity": "sha512-cAVTI2VLHWYsGOirfeYVVQ7ZDejtQ9fp4YhYckWDEkFfqbVjaT11iM8k6xSAfGFMM+gDpZjMnFssPu8we+mqFw==",
-      "dev": true,
       "requires": {
       "requires": {
         "path-parse": "^1.0.6"
         "path-parse": "^1.0.6"
       }
       }
@@ -16875,8 +16914,7 @@
     "strip-bom": {
     "strip-bom": {
       "version": "3.0.0",
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
-      "dev": true
+      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
     },
     },
     "strip-comments": {
     "strip-comments": {
       "version": "1.0.2",
       "version": "1.0.2",
@@ -17351,6 +17389,15 @@
       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
       "dev": true
       "dev": true
     },
     },
+    "tiny-glob": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.6.tgz",
+      "integrity": "sha512-A7ewMqPu1B5PWwC3m7KVgAu96Ch5LA0w4SnEN/LbDREj/gAD0nPWboRbn8YoP9ISZXqeNAlMvKSKoEuhcfK3Pw==",
+      "requires": {
+        "globalyzer": "^0.1.0",
+        "globrex": "^0.1.1"
+      }
+    },
     "tiny-invariant": {
     "tiny-invariant": {
       "version": "1.0.6",
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
       "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
@@ -17477,6 +17524,27 @@
       "integrity": "sha512-ti7OGMOUOzo66wLF3liskw6YQIaSsBgc4GOAlWRnIEj8htCxJUxskanMUoJOD6MDCRAXo36goXJZch+nOS0VMA==",
       "integrity": "sha512-ti7OGMOUOzo66wLF3liskw6YQIaSsBgc4GOAlWRnIEj8htCxJUxskanMUoJOD6MDCRAXo36goXJZch+nOS0VMA==",
       "dev": true
       "dev": true
     },
     },
+    "tsconfig-paths": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz",
+      "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==",
+      "requires": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.1",
+        "minimist": "^1.2.0",
+        "strip-bom": "^3.0.0"
+      },
+      "dependencies": {
+        "json5": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+          "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+          "requires": {
+            "minimist": "^1.2.0"
+          }
+        }
+      }
+    },
     "tslib": {
     "tslib": {
       "version": "1.10.0",
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
@@ -17806,6 +17874,11 @@
       "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
       "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
       "dev": true
       "dev": true
     },
     },
+    "use-debounce": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-3.4.3.tgz",
+      "integrity": "sha512-nxy+opOxDccWfhMl36J5BSCTpvcj89iaQk2OZWLAtBJQj7ISCtx1gh+rFbdjGfMl6vtCZf6gke/kYvrkVfHMoA=="
+    },
     "util": {
     "util": {
       "version": "0.10.3",
       "version": "0.10.3",
       "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
       "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",

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

@@ -8,6 +8,7 @@
     "bulma-switch": "^2.0.0",
     "bulma-switch": "^2.0.0",
     "classnames": "^2.2.6",
     "classnames": "^2.2.6",
     "date-fns": "^2.14.0",
     "date-fns": "^2.14.0",
+    "eslint-import-resolver-typescript": "^2.0.0",
     "immer": "^6.0.5",
     "immer": "^6.0.5",
     "lodash": "^4.17.15",
     "lodash": "^4.17.15",
     "pretty-ms": "^6.0.1",
     "pretty-ms": "^6.0.1",
@@ -15,12 +16,14 @@
     "react-datepicker": "^3.0.0",
     "react-datepicker": "^3.0.0",
     "react-dom": "^16.12.0",
     "react-dom": "^16.12.0",
     "react-hook-form": "^4.5.5",
     "react-hook-form": "^4.5.5",
+    "react-multi-select-component": "^2.0.12",
     "react-redux": "^7.1.3",
     "react-redux": "^7.1.3",
     "react-router-dom": "^5.1.2",
     "react-router-dom": "^5.1.2",
     "redux": "^4.0.5",
     "redux": "^4.0.5",
     "redux-thunk": "^2.3.0",
     "redux-thunk": "^2.3.0",
     "reselect": "^4.0.0",
     "reselect": "^4.0.0",
-    "typesafe-actions": "^5.1.0"
+    "typesafe-actions": "^5.1.0",
+    "use-debounce": "^3.4.3"
   },
   },
   "lint-staged": {
   "lint-staged": {
     "*.{js,ts,jsx,tsx}": [
     "*.{js,ts,jsx,tsx}": [

+ 155 - 41
kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx

@@ -1,10 +1,12 @@
-import React, { useCallback, useEffect, useRef } from 'react';
+import React, { useEffect, useRef } from 'react';
 import {
 import {
   ClusterName,
   ClusterName,
+  SeekType,
   SeekTypes,
   SeekTypes,
   TopicMessage,
   TopicMessage,
   TopicMessageQueryParams,
   TopicMessageQueryParams,
   TopicName,
   TopicName,
+  TopicPartition,
 } from 'redux/interfaces';
 } from 'redux/interfaces';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
@@ -15,7 +17,11 @@ import CustomParamButton, {
   CustomParamButtonType,
   CustomParamButtonType,
 } from 'components/Topics/shared/Form/CustomParams/CustomParamButton';
 } from 'components/Topics/shared/Form/CustomParams/CustomParamButton';
 
 
-import { debounce } from 'lodash';
+import MultiSelect from 'react-multi-select-component';
+
+import * as _ from 'lodash';
+import { useDebouncedCallback } from 'use-debounce';
+import { Option } from 'react-multi-select-component/dist/lib/interfaces';
 
 
 interface Props {
 interface Props {
   clusterName: ClusterName;
   clusterName: ClusterName;
@@ -27,6 +33,7 @@ interface Props {
     queryParams: Partial<TopicMessageQueryParams>
     queryParams: Partial<TopicMessageQueryParams>
   ) => void;
   ) => void;
   messages: TopicMessage[];
   messages: TopicMessage[];
+  partitions: TopicPartition[];
 }
 }
 
 
 interface FilterProps {
 interface FilterProps {
@@ -47,6 +54,7 @@ const Messages: React.FC<Props> = ({
   clusterName,
   clusterName,
   topicName,
   topicName,
   messages,
   messages,
+  partitions,
   fetchTopicMessages,
   fetchTopicMessages,
 }) => {
 }) => {
   const [searchQuery, setSearchQuery] = React.useState<string>('');
   const [searchQuery, setSearchQuery] = React.useState<string>('');
@@ -54,23 +62,51 @@ const Messages: React.FC<Props> = ({
     null
     null
   );
   );
   const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]);
   const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]);
+  const [selectedSeekType, setSelectedSeekType] = React.useState<SeekType>(
+    SeekTypes.OFFSET
+  );
+  const [searchOffset, setSearchOffset] = React.useState<string>('0');
+  const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(
+    []
+  );
   const [queryParams, setQueryParams] = React.useState<
   const [queryParams, setQueryParams] = React.useState<
     Partial<TopicMessageQueryParams>
     Partial<TopicMessageQueryParams>
   >({ limit: 100 });
   >({ limit: 100 });
+  const [debouncedCallback] = useDebouncedCallback(
+    (query: any) => setQueryParams({ ...queryParams, ...query }),
+    1000
+  );
 
 
   const prevSearchTimestamp = usePrevious(searchTimestamp);
   const prevSearchTimestamp = usePrevious(searchTimestamp);
 
 
   const getUniqueDataForEachPartition: FilterProps[] = React.useMemo(() => {
   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]);
+    const partitionUniqs: FilterProps[] = partitions.map((p) => ({
+      offset: 0,
+      partition: p.partition,
+    }));
+    const messageUniqs: FilterProps[] = _.map(
+      _.groupBy(messages, 'partition'),
+      (v) => _.maxBy(v, 'offset')
+    ).map((v) => ({
+      offset: v ? v.offset : 0,
+      partition: v ? v.partition : 0,
+    }));
+
+    return _.map(
+      _.groupBy(_.concat(partitionUniqs, messageUniqs), 'partition'),
+      (v) => _.maxBy(v, 'offset') as FilterProps
+    );
+  }, [messages, partitions]);
+
+  const getSeekToValuesForPartitions = (partition: any) => {
+    const foundedValues = filterProps.find(
+      (prop) => prop.partition === partition.value
+    );
+    if (selectedSeekType === SeekTypes.OFFSET) {
+      return foundedValues ? foundedValues.offset : 0;
+    }
+    return searchTimestamp ? searchTimestamp.getTime() : null;
+  };
 
 
   React.useEffect(() => {
   React.useEffect(() => {
     fetchTopicMessages(clusterName, topicName, queryParams);
     fetchTopicMessages(clusterName, topicName, queryParams);
@@ -78,20 +114,13 @@ const Messages: React.FC<Props> = ({
 
 
   React.useEffect(() => {
   React.useEffect(() => {
     setFilterProps(getUniqueDataForEachPartition);
     setFilterProps(getUniqueDataForEachPartition);
-  }, [messages]);
+  }, [messages, partitions]);
 
 
-  const handleDelayedQuery = useCallback(
-    debounce(
-      (query: string) => setQueryParams({ ...queryParams, q: query }),
-      1000
-    ),
-    []
-  );
   const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
   const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     const query = event.target.value;
     const query = event.target.value;
 
 
     setSearchQuery(query);
     setSearchQuery(query);
-    handleDelayedQuery(query);
+    debouncedCallback({ q: query });
   };
   };
 
 
   const handleDateTimeChange = () => {
   const handleDateTimeChange = () => {
@@ -103,7 +132,7 @@ const Messages: React.FC<Props> = ({
         setQueryParams({
         setQueryParams({
           ...queryParams,
           ...queryParams,
           seekType: SeekTypes.TIMESTAMP,
           seekType: SeekTypes.TIMESTAMP,
-          seekTo: filterProps.map((p) => `${p.partition}::${timestamp}`),
+          seekTo: selectedPartitions.map((p) => `${p.value}::${timestamp}`),
         });
         });
       } else {
       } else {
         setSearchTimestamp(null);
         setSearchTimestamp(null);
@@ -113,6 +142,33 @@ const Messages: React.FC<Props> = ({
     }
     }
   };
   };
 
 
+  const handleSeekTypeChange = (
+    event: React.ChangeEvent<HTMLSelectElement>
+  ) => {
+    setSelectedSeekType(event.target.value as SeekType);
+  };
+
+  const handleOffsetChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const offset = event.target.value || '0';
+    setSearchOffset(offset);
+    debouncedCallback({
+      seekType: SeekTypes.OFFSET,
+      seekTo: selectedPartitions.map((p) => `${p.value}::${offset}`),
+    });
+  };
+
+  const handlePartitionsChange = (options: Option[]) => {
+    setSelectedPartitions(options);
+
+    debouncedCallback({
+      seekType: options.length > 0 ? selectedSeekType : undefined,
+      seekTo:
+        options.length > 0
+          ? options.map((p) => `${p.value}::${getSeekToValuesForPartitions(p)}`)
+          : undefined,
+    });
+  };
+
   const getTimestampDate = (timestamp: number) => {
   const getTimestampDate = (timestamp: number) => {
     return format(new Date(timestamp * 1000), 'MM.dd.yyyy HH:mm:ss');
     return format(new Date(timestamp * 1000), 'MM.dd.yyyy HH:mm:ss');
   };
   };
@@ -150,9 +206,13 @@ const Messages: React.FC<Props> = ({
   const onNext = (event: React.MouseEvent<HTMLButtonElement>) => {
   const onNext = (event: React.MouseEvent<HTMLButtonElement>) => {
     event.preventDefault();
     event.preventDefault();
 
 
-    const seekTo: string[] = filterProps.map(
-      (p) => `${p.partition}::${p.offset}`
-    );
+    const seekTo: string[] = filterProps
+      .filter(
+        (value) =>
+          selectedPartitions.findIndex((p) => p.value === value.partition) > -1
+      )
+      .map((p) => `${p.partition}::${p.offset}`);
+
     setQueryParams({
     setQueryParams({
       ...queryParams,
       ...queryParams,
       seekType: SeekTypes.OFFSET,
       seekType: SeekTypes.OFFSET,
@@ -160,6 +220,15 @@ const Messages: React.FC<Props> = ({
     });
     });
   };
   };
 
 
+  const filterOptions = (options: Option[], filter: any) => {
+    if (!filter) {
+      return options;
+    }
+    return options.filter(
+      ({ value }) => value.toString() && value.toString() === filter
+    );
+  };
+
   const getTopicMessagesTable = () => {
   const getTopicMessagesTable = () => {
     return messages.length > 0 ? (
     return messages.length > 0 ? (
       <div>
       <div>
@@ -199,23 +268,70 @@ const Messages: React.FC<Props> = ({
     );
     );
   };
   };
 
 
-  return isFetched ? (
+  if (!isFetched) {
+    return <PageLoader isFullHeight={false} />;
+  }
+
+  return (
     <div>
     <div>
       <div className="columns">
       <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 className="column is-one-fifth">
+          <label className="label">Partitions</label>
+          <MultiSelect
+            options={partitions.map((p) => ({
+              label: `Partition #${p.partition.toString()}`,
+              value: p.partition,
+            }))}
+            filterOptions={filterOptions}
+            value={selectedPartitions}
+            onChange={handlePartitionsChange}
+            labelledBy="Select partitions"
           />
           />
         </div>
         </div>
-        <div className="column is-two-quarters is-offset-one-quarter">
+        <div className="column is-one-fifth">
+          <label className="label">Seek Type</label>
+          <div className="select is-block">
+            <select
+              id="selectSeekType"
+              name="selectSeekType"
+              onChange={handleSeekTypeChange}
+              defaultValue={SeekTypes.OFFSET}
+              value={selectedSeekType}
+            >
+              <option value={SeekTypes.OFFSET}>Offset</option>
+              <option value={SeekTypes.TIMESTAMP}>Timestamp</option>
+            </select>
+          </div>
+        </div>
+        <div className="column is-one-fifth">
+          {selectedSeekType === SeekTypes.OFFSET ? (
+            <>
+              <label className="label">Offset</label>
+              <input
+                id="searchOffset"
+                name="searchOffset"
+                type="text"
+                className="input"
+                value={searchOffset}
+                onChange={handleOffsetChange}
+              />
+            </>
+          ) : (
+            <>
+              <label className="label">Timestamp</label>
+              <DatePicker
+                selected={searchTimestamp}
+                onChange={(date) => setSearchTimestamp(date)}
+                onCalendarClose={handleDateTimeChange}
+                showTimeInput
+                timeInputLabel="Time:"
+                dateFormat="MMMM d, yyyy h:mm aa"
+                className="input"
+              />
+            </>
+          )}
+        </div>
+        <div className="column is-two-fifths">
           <label className="label">Search</label>
           <label className="label">Search</label>
           <input
           <input
             id="searchText"
             id="searchText"
@@ -228,10 +344,8 @@ const Messages: React.FC<Props> = ({
           />
           />
         </div>
         </div>
       </div>
       </div>
-      <div>{getTopicMessagesTable()}</div>
+      {getTopicMessagesTable()}
     </div>
     </div>
-  ) : (
-    <PageLoader isFullHeight={false} />
   );
   );
 };
 };
 
 

+ 2 - 0
kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesContainer.ts

@@ -9,6 +9,7 @@ import { RouteComponentProps, withRouter } from 'react-router-dom';
 import { fetchTopicMessages } from 'redux/actions';
 import { fetchTopicMessages } from 'redux/actions';
 import {
 import {
   getIsTopicMessagesFetched,
   getIsTopicMessagesFetched,
+  getPartitionsByTopicName,
   getTopicMessages,
   getTopicMessages,
 } from 'redux/reducers/topics/selectors';
 } from 'redux/reducers/topics/selectors';
 
 
@@ -33,6 +34,7 @@ const mapStateToProps = (
   topicName,
   topicName,
   isFetched: getIsTopicMessagesFetched(state),
   isFetched: getIsTopicMessagesFetched(state),
   messages: getTopicMessages(state),
   messages: getTopicMessages(state),
+  partitions: getPartitionsByTopicName(state, topicName),
 });
 });
 
 
 const mapDispatchToProps = {
 const mapDispatchToProps = {

+ 3 - 1
kafka-ui-react-app/src/redux/api/topics.ts

@@ -59,7 +59,9 @@ export const getTopicMessages = (
     const value = entry[1];
     const value = entry[1];
     if (value) {
     if (value) {
       if (Array.isArray(value)) {
       if (Array.isArray(value)) {
-        searchParams += value.map((v) => `${key}=${v}&`);
+        value.forEach((v) => {
+          searchParams += `${key}=${v}&`;
+        });
       } else {
       } else {
         searchParams += `${key}=${value}&`;
         searchParams += `${key}=${value}&`;
       }
       }

+ 6 - 0
kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

@@ -80,6 +80,12 @@ export const getTopicByName = createSelector(
   (topics, topicName) => topics[topicName]
   (topics, topicName) => topics[topicName]
 );
 );
 
 
+export const getPartitionsByTopicName = createSelector(
+  getTopicMap,
+  getTopicName,
+  (topics, topicName) => topics[topicName].partitions
+);
+
 export const getFullTopic = createSelector(getTopicByName, (topic) =>
 export const getFullTopic = createSelector(getTopicByName, (topic) =>
   topic && topic.config && !!topic.partitionCount ? topic : undefined
   topic && topic.config && !!topic.partitionCount ? topic : undefined
 );
 );