123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- import React, { useCallback, useEffect, useRef } from 'react';
- import {
- ClusterName,
- TopicMessageQueryParams,
- TopicName,
- } from 'redux/interfaces';
- import { TopicMessage, Partition, SeekType } from 'generated-sources';
- import PageLoader from 'components/common/PageLoader/PageLoader';
- import DatePicker from 'react-datepicker';
- import 'react-datepicker/dist/react-datepicker.css';
- import CustomParamButton, {
- CustomParamButtonType,
- } from 'components/Topics/shared/Form/CustomParams/CustomParamButton';
- 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';
- import MessagesTable from './MessagesTable';
- export interface Props {
- clusterName: ClusterName;
- topicName: TopicName;
- isFetched: boolean;
- fetchTopicMessages: (
- clusterName: ClusterName,
- topicName: TopicName,
- queryParams: Partial<TopicMessageQueryParams>
- ) => void;
- messages: TopicMessage[];
- partitions: Partition[];
- }
- interface FilterProps {
- offset: TopicMessage['offset'];
- partition: TopicMessage['partition'];
- }
- function usePrevious(value: Date | null) {
- const ref = useRef<Date | null>();
- useEffect(() => {
- ref.current = value;
- });
- return ref.current;
- }
- const Messages: React.FC<Props> = ({
- isFetched,
- clusterName,
- topicName,
- messages,
- partitions,
- fetchTopicMessages,
- }) => {
- const [searchQuery, setSearchQuery] = React.useState<string>('');
- const [searchTimestamp, setSearchTimestamp] = React.useState<Date | null>(
- null
- );
- const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]);
- const [selectedSeekType, setSelectedSeekType] = React.useState<SeekType>(
- SeekType.OFFSET
- );
- const [searchOffset, setSearchOffset] = React.useState<string>();
- const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(
- partitions.map((p) => ({
- value: p.partition,
- label: p.partition.toString(),
- }))
- );
- const [queryParams, setQueryParams] = React.useState<
- Partial<TopicMessageQueryParams>
- >({ limit: 100 });
- const [debouncedCallback] = useDebouncedCallback(
- (query: Partial<TopicMessageQueryParams>) =>
- setQueryParams({ ...queryParams, ...query }),
- 1000
- );
- const prevSearchTimestamp = usePrevious(searchTimestamp);
- const getUniqueDataForEachPartition: FilterProps[] = React.useMemo(() => {
- 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: Option) => {
- const foundedValues = filterProps.find(
- (prop) => prop.partition === partition.value
- );
- if (selectedSeekType === SeekType.OFFSET) {
- return foundedValues ? foundedValues.offset : 0;
- }
- return searchTimestamp ? searchTimestamp.getTime() : null;
- };
- React.useEffect(() => {
- fetchTopicMessages(clusterName, topicName, queryParams);
- }, []);
- React.useEffect(() => {
- setFilterProps(getUniqueDataForEachPartition);
- }, [messages, partitions]);
- const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- const query = event.target.value;
- setSearchQuery(query);
- debouncedCallback({ q: query });
- };
- const handleDateTimeChange = () => {
- if (searchTimestamp !== prevSearchTimestamp) {
- if (searchTimestamp) {
- const timestamp: number = searchTimestamp.getTime();
- setSearchTimestamp(searchTimestamp);
- setQueryParams({
- ...queryParams,
- seekType: SeekType.TIMESTAMP,
- seekTo: selectedPartitions.map((p) => `${p.value}::${timestamp}`),
- });
- } else {
- setSearchTimestamp(null);
- const { seekTo, seekType, ...queryParamsWithoutSeek } = queryParams;
- setQueryParams(queryParamsWithoutSeek);
- }
- }
- };
- 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: SeekType.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 handleFiltersSubmit = useCallback(() => {
- fetchTopicMessages(clusterName, topicName, queryParams);
- }, [clusterName, topicName, queryParams]);
- const onNext = (event: React.MouseEvent<HTMLButtonElement>) => {
- event.preventDefault();
- const seekTo: string[] = filterProps
- .filter(
- (value) =>
- selectedPartitions.findIndex((p) => p.value === value.partition) > -1
- )
- .map((p) => `${p.partition}::${p.offset}`);
- fetchTopicMessages(clusterName, topicName, {
- ...queryParams,
- seekType: SeekType.OFFSET,
- seekTo,
- });
- };
- const filterOptions = (options: Option[], filter: string) => {
- if (!filter) {
- return options;
- }
- return options.filter(
- ({ value }) => value.toString() && value.toString() === filter
- );
- };
- const getTopicMessagesTable = () => {
- return messages.length > 0 ? (
- <div>
- <MessagesTable messages={messages} />
- <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>
- ) : (
- <div>No messages at selected topic</div>
- );
- };
- if (!isFetched) {
- return <PageLoader isFullHeight={false} />;
- }
- return (
- <div>
- <div className="columns">
- <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}
- value={selectedSeekType}
- >
- <option value={SeekType.OFFSET}>Offset</option>
- <option value={SeekType.TIMESTAMP}>Timestamp</option>
- </select>
- </div>
- </div>
- <div className="column is-one-fifth">
- {selectedSeekType === SeekType.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: Date | null) => 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>
- <input
- id="searchText"
- type="text"
- name="searchText"
- className="input"
- placeholder="Search"
- value={searchQuery}
- onChange={handleQueryChange}
- />
- </div>
- </div>
- <div className="columns">
- <div className="column is-full" style={{ textAlign: 'right' }}>
- <input
- type="submit"
- className="button is-primary"
- onClick={handleFiltersSubmit}
- />
- </div>
- </div>
- {getTopicMessagesTable()}
- </div>
- );
- };
- export default Messages;
|