|
1 | | -import React from 'react'; |
2 | | -import { ClusterName, TopicMessage, TopicName } from 'redux/interfaces'; |
| 1 | +import React, { useCallback, useEffect, useRef } from 'react'; |
| 2 | +import { |
| 3 | + ClusterName, |
| 4 | + SeekTypes, |
| 5 | + TopicMessage, |
| 6 | + TopicMessageQueryParams, |
| 7 | + TopicName, |
| 8 | +} from 'redux/interfaces'; |
3 | 9 | import PageLoader from 'components/common/PageLoader/PageLoader'; |
4 | 10 | import { format } from 'date-fns'; |
| 11 | +import DatePicker from 'react-datepicker'; |
| 12 | + |
| 13 | +import 'react-datepicker/dist/react-datepicker.css'; |
| 14 | +import CustomParamButton, { |
| 15 | + CustomParamButtonType, |
| 16 | +} from 'components/Topics/shared/Form/CustomParams/CustomParamButton'; |
| 17 | + |
| 18 | +import { debounce } from 'lodash'; |
5 | 19 |
|
6 | 20 | interface Props { |
7 | 21 | clusterName: ClusterName; |
8 | 22 | topicName: TopicName; |
9 | 23 | isFetched: boolean; |
10 | | - fetchTopicMessages: (clusterName: ClusterName, topicName: TopicName) => void; |
| 24 | + fetchTopicMessages: ( |
| 25 | + clusterName: ClusterName, |
| 26 | + topicName: TopicName, |
| 27 | + queryParams: Partial<TopicMessageQueryParams> |
| 28 | + ) => void; |
11 | 29 | messages: TopicMessage[]; |
12 | 30 | } |
13 | 31 |
|
| 32 | +interface FilterProps { |
| 33 | + offset: number; |
| 34 | + partition: number; |
| 35 | +} |
| 36 | + |
| 37 | +function usePrevious(value: any) { |
| 38 | + const ref = useRef(); |
| 39 | + useEffect(() => { |
| 40 | + ref.current = value; |
| 41 | + }); |
| 42 | + return ref.current; |
| 43 | +} |
| 44 | + |
14 | 45 | const Messages: React.FC<Props> = ({ |
15 | 46 | isFetched, |
16 | 47 | clusterName, |
17 | 48 | topicName, |
18 | 49 | messages, |
19 | 50 | fetchTopicMessages, |
20 | 51 | }) => { |
| 52 | + const [searchQuery, setSearchQuery] = React.useState<string>(''); |
| 53 | + const [searchTimestamp, setSearchTimestamp] = React.useState<Date | null>( |
| 54 | + null |
| 55 | + ); |
| 56 | + const [filterProps, setFilterProps] = React.useState<FilterProps[]>([]); |
| 57 | + const [queryParams, setQueryParams] = React.useState< |
| 58 | + Partial<TopicMessageQueryParams> |
| 59 | + >({ limit: 100 }); |
| 60 | + |
| 61 | + const prevSearchTimestamp = usePrevious(searchTimestamp); |
| 62 | + |
| 63 | + const getUniqueDataForEachPartition: FilterProps[] = React.useMemo(() => { |
| 64 | + const map = messages.map((message) => [ |
| 65 | + message.partition, |
| 66 | + { |
| 67 | + partition: message.partition, |
| 68 | + offset: message.offset, |
| 69 | + }, |
| 70 | + ]); |
| 71 | + // @ts-ignore |
| 72 | + return [...new Map(map).values()]; |
| 73 | + }, [messages]); |
| 74 | + |
| 75 | + React.useEffect(() => { |
| 76 | + fetchTopicMessages(clusterName, topicName, queryParams); |
| 77 | + }, [fetchTopicMessages, clusterName, topicName, queryParams]); |
| 78 | + |
21 | 79 | React.useEffect(() => { |
22 | | - fetchTopicMessages(clusterName, topicName); |
23 | | - }, [fetchTopicMessages, clusterName, topicName]); |
| 80 | + setFilterProps(getUniqueDataForEachPartition); |
| 81 | + }, [messages]); |
| 82 | + |
| 83 | + const handleDelayedQuery = useCallback( |
| 84 | + debounce( |
| 85 | + (query: string) => setQueryParams({ ...queryParams, q: query }), |
| 86 | + 1000 |
| 87 | + ), |
| 88 | + [] |
| 89 | + ); |
| 90 | + const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => { |
| 91 | + const query = event.target.value; |
| 92 | + |
| 93 | + setSearchQuery(query); |
| 94 | + handleDelayedQuery(query); |
| 95 | + }; |
24 | 96 |
|
25 | | - const [searchText, setSearchText] = React.useState<string>(''); |
| 97 | + const handleDateTimeChange = () => { |
| 98 | + if (searchTimestamp !== prevSearchTimestamp) { |
| 99 | + if (searchTimestamp) { |
| 100 | + const timestamp: number = searchTimestamp.getTime(); |
26 | 101 |
|
27 | | - const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { |
28 | | - setSearchText(event.target.value); |
| 102 | + setSearchTimestamp(searchTimestamp); |
| 103 | + setQueryParams({ |
| 104 | + ...queryParams, |
| 105 | + seekType: SeekTypes.TIMESTAMP, |
| 106 | + seekTo: filterProps.map((p) => `${p.partition}::${timestamp}`), |
| 107 | + }); |
| 108 | + } else { |
| 109 | + setSearchTimestamp(null); |
| 110 | + const { seekTo, seekType, ...queryParamsWithoutSeek } = queryParams; |
| 111 | + setQueryParams(queryParamsWithoutSeek); |
| 112 | + } |
| 113 | + } |
29 | 114 | }; |
30 | 115 |
|
31 | 116 | const getTimestampDate = (timestamp: number) => { |
32 | 117 | return format(new Date(timestamp * 1000), 'MM.dd.yyyy HH:mm:ss'); |
33 | 118 | }; |
34 | 119 |
|
35 | | - const getMessageContentHeaders = () => { |
| 120 | + const getMessageContentHeaders = React.useMemo(() => { |
36 | 121 | const message = messages[0]; |
37 | 122 | const headers: JSX.Element[] = []; |
38 | | - const content = JSON.parse(message.content); |
39 | | - Object.keys(content).forEach((k) => |
40 | | - headers.push(<th>{`content.${k}`}</th>) |
41 | | - ); |
42 | | - |
| 123 | + try { |
| 124 | + const content = |
| 125 | + typeof message.content !== 'object' |
| 126 | + ? JSON.parse(message.content) |
| 127 | + : message.content; |
| 128 | + Object.keys(content).forEach((k) => |
| 129 | + headers.push(<th key={Math.random()}>{`content.${k}`}</th>) |
| 130 | + ); |
| 131 | + } catch (e) { |
| 132 | + headers.push(<th>Content</th>); |
| 133 | + } |
43 | 134 | return headers; |
44 | | - }; |
| 135 | + }, [messages]); |
45 | 136 |
|
46 | | - const getMessageContentBody = (content: string) => { |
47 | | - const c = JSON.parse(content); |
| 137 | + const getMessageContentBody = (content: any) => { |
48 | 138 | const columns: JSX.Element[] = []; |
49 | | - Object.values(c).map((v) => columns.push(<td>{JSON.stringify(v)}</td>)); |
| 139 | + try { |
| 140 | + const c = typeof content !== 'object' ? JSON.parse(content) : content; |
| 141 | + Object.values(c).map((v) => |
| 142 | + columns.push(<td key={Math.random()}>{JSON.stringify(v)}</td>) |
| 143 | + ); |
| 144 | + } catch (e) { |
| 145 | + columns.push(<td>{content}</td>); |
| 146 | + } |
50 | 147 | return columns; |
51 | 148 | }; |
52 | 149 |
|
53 | | - return ( |
54 | | - // eslint-disable-next-line no-nested-ternary |
55 | | - isFetched ? ( |
56 | | - messages.length > 0 ? ( |
57 | | - <div> |
58 | | - <div className="columns"> |
59 | | - <div className="column is-half is-offset-half"> |
60 | | - <input |
61 | | - id="searchText" |
62 | | - type="text" |
63 | | - name="searchText" |
64 | | - className="input" |
65 | | - placeholder="Search" |
66 | | - value={searchText} |
67 | | - onChange={handleInputChange} |
68 | | - /> |
69 | | - </div> |
70 | | - </div> |
71 | | - <table className="table is-striped is-fullwidth"> |
72 | | - <thead> |
73 | | - <tr> |
74 | | - <th>Timestamp</th> |
75 | | - <th>Offset</th> |
76 | | - <th>Partition</th> |
77 | | - {getMessageContentHeaders()} |
| 150 | + const onNext = (event: React.MouseEvent<HTMLButtonElement>) => { |
| 151 | + event.preventDefault(); |
| 152 | + |
| 153 | + const seekTo: string[] = filterProps.map( |
| 154 | + (p) => `${p.partition}::${p.offset}` |
| 155 | + ); |
| 156 | + setQueryParams({ |
| 157 | + ...queryParams, |
| 158 | + seekType: SeekTypes.OFFSET, |
| 159 | + seekTo, |
| 160 | + }); |
| 161 | + }; |
| 162 | + |
| 163 | + const getTopicMessagesTable = () => { |
| 164 | + return messages.length > 0 ? ( |
| 165 | + <div> |
| 166 | + <table className="table is-striped is-fullwidth"> |
| 167 | + <thead> |
| 168 | + <tr> |
| 169 | + <th>Timestamp</th> |
| 170 | + <th>Offset</th> |
| 171 | + <th>Partition</th> |
| 172 | + {getMessageContentHeaders} |
| 173 | + </tr> |
| 174 | + </thead> |
| 175 | + <tbody> |
| 176 | + {messages.map((message) => ( |
| 177 | + <tr key={`${message.timestamp}${Math.random()}`}> |
| 178 | + <td>{getTimestampDate(message.timestamp)}</td> |
| 179 | + <td>{message.offset}</td> |
| 180 | + <td>{message.partition}</td> |
| 181 | + {getMessageContentBody(message.content)} |
78 | 182 | </tr> |
79 | | - </thead> |
80 | | - <tbody> |
81 | | - {messages |
82 | | - .filter( |
83 | | - (message) => |
84 | | - !searchText || message?.content?.indexOf(searchText) >= 0 |
85 | | - ) |
86 | | - .map((message) => ( |
87 | | - <tr key={message.timestamp}> |
88 | | - <td>{getTimestampDate(message.timestamp)}</td> |
89 | | - <td>{message.offset}</td> |
90 | | - <td>{message.partition}</td> |
91 | | - {getMessageContentBody(message.content)} |
92 | | - </tr> |
93 | | - ))} |
94 | | - </tbody> |
95 | | - </table> |
| 183 | + ))} |
| 184 | + </tbody> |
| 185 | + </table> |
| 186 | + <div className="columns"> |
| 187 | + <div className="column is-full"> |
| 188 | + <CustomParamButton |
| 189 | + className="is-link is-pulled-right" |
| 190 | + type={CustomParamButtonType.chevronRight} |
| 191 | + onClick={onNext} |
| 192 | + btnText="Next" |
| 193 | + /> |
| 194 | + </div> |
96 | 195 | </div> |
97 | | - ) : ( |
98 | | - <div>No messages at selected topic</div> |
99 | | - ) |
| 196 | + </div> |
100 | 197 | ) : ( |
101 | | - <PageLoader isFullHeight={false} /> |
102 | | - ) |
| 198 | + <div>No messages at selected topic</div> |
| 199 | + ); |
| 200 | + }; |
| 201 | + |
| 202 | + return isFetched ? ( |
| 203 | + <div> |
| 204 | + <div className="columns"> |
| 205 | + <div className="column is-one-quarter"> |
| 206 | + <label className="label">Timestamp</label> |
| 207 | + <DatePicker |
| 208 | + selected={searchTimestamp} |
| 209 | + onChange={(date) => setSearchTimestamp(date)} |
| 210 | + onCalendarClose={handleDateTimeChange} |
| 211 | + isClearable |
| 212 | + showTimeInput |
| 213 | + timeInputLabel="Time:" |
| 214 | + dateFormat="MMMM d, yyyy h:mm aa" |
| 215 | + className="input" |
| 216 | + /> |
| 217 | + </div> |
| 218 | + <div className="column is-two-quarters is-offset-one-quarter"> |
| 219 | + <label className="label">Search</label> |
| 220 | + <input |
| 221 | + id="searchText" |
| 222 | + type="text" |
| 223 | + name="searchText" |
| 224 | + className="input" |
| 225 | + placeholder="Search" |
| 226 | + value={searchQuery} |
| 227 | + onChange={handleQueryChange} |
| 228 | + /> |
| 229 | + </div> |
| 230 | + </div> |
| 231 | + <div>{getTopicMessagesTable()}</div> |
| 232 | + </div> |
| 233 | + ) : ( |
| 234 | + <PageLoader isFullHeight={false} /> |
103 | 235 | ); |
104 | 236 | }; |
105 | 237 |
|
|
0 commit comments