Topic messages filtering paginating (#72)

* Add filtering and pagination for topic messages

* Add delay to search query, momoize some functions

* Add partition selection
This commit is contained in:
maxim_tereshin 2020-07-07 14:45:34 +05:00 committed by GitHub
parent 2cb630dc18
commit be2d38133d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 258 additions and 57 deletions

View file

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

View file

@ -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=", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
"dev": true
}, },
"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==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"dev": true
}, },
"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==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"dev": true
}, },
"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==", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
"dev": true
}, },
"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=", "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
"dev": true
}, },
"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",

View file

@ -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}": [

View file

@ -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) => [ const partitionUniqs: FilterProps[] = partitions.map((p) => ({
message.partition, offset: 0,
{ partition: p.partition,
partition: message.partition, }));
offset: message.offset, const messageUniqs: FilterProps[] = _.map(
}, _.groupBy(messages, 'partition'),
]); (v) => _.maxBy(v, 'offset')
// @ts-ignore ).map((v) => ({
return [...new Map(map).values()]; offset: v ? v.offset : 0,
}, [messages]); 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( const seekTo: string[] = filterProps
(p) => `${p.partition}::${p.offset}` .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"> <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 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> <label className="label">Timestamp</label>
<DatePicker <DatePicker
selected={searchTimestamp} selected={searchTimestamp}
onChange={(date) => setSearchTimestamp(date)} onChange={(date) => setSearchTimestamp(date)}
onCalendarClose={handleDateTimeChange} onCalendarClose={handleDateTimeChange}
isClearable
showTimeInput showTimeInput
timeInputLabel="Time:" timeInputLabel="Time:"
dateFormat="MMMM d, yyyy h:mm aa" dateFormat="MMMM d, yyyy h:mm aa"
className="input" className="input"
/> />
</>
)}
</div> </div>
<div className="column is-two-quarters is-offset-one-quarter"> <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} />
); );
}; };

View file

@ -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 = {

View file

@ -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}&`;
} }

View file

@ -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
); );