Issues/1740 live tailing improvements (#1774)

* Implementing Context to the Topic messages pages

* Using TopicContext in the Topics Topic MessageTable component

* Using TopicContext variable in the Filters component

* Fixing the Ordering of the Live mode Topic messaging

* Fixing isLive parameter bug during page refresh

* Minor code modification in Topic Filter Message page

* Implement the correct seekType during live mode in url as well as in api call

* Add Test cases to Messages and refactor eventSource Mock

* Add initial Testing file for messages table

* improve the MessagesTable test File

* improve the MessagesTable test File + Filter Test File

* improve the MessagesTable test File

* Change the function name toggleSeekDirection  to changeSeekDirection

* change the name of the test suites to be more declarative

* Display the table progress bar in live mode only when no data is fetched
This commit is contained in:
Mgrdich 2022-04-05 16:04:41 +04:00 committed by GitHub
parent c79905ce32
commit 68f8eed8f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 325 additions and 96 deletions

View file

@ -1,4 +1,4 @@
import { render } from 'lib/testHelpers';
import { render, EventSourceMock } from 'lib/testHelpers';
import React from 'react';
import Query, {
getFormattedErrorFromTableData,
@ -20,27 +20,6 @@ const renderComponent = () =>
}
);
// Small mock to get rid of reference error
class EventSourceMock {
url: string;
close: () => void;
open: () => void;
error: () => void;
onmessage: () => void;
constructor(url: string) {
this.url = url;
this.open = jest.fn();
this.error = jest.fn();
this.onmessage = jest.fn();
this.close = jest.fn();
}
}
describe('Query', () => {
it('renders', () => {
renderComponent();

View file

@ -10,7 +10,7 @@ import {
TopicMessageEventTypeEnum,
MessageFilterType,
} from 'generated-sources';
import React from 'react';
import React, { useContext } from 'react';
import { omitBy } from 'lodash';
import { useHistory, useLocation } from 'react-router';
import DatePicker from 'react-datepicker';
@ -25,6 +25,8 @@ import { Button } from 'components/common/Button/Button';
import FilterModal, {
FilterEdit,
} from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
import * as S from './Filters.styled';
import {
@ -66,11 +68,6 @@ export const SeekTypeOptions = [
{ value: SeekType.OFFSET, label: 'Offset' },
{ value: SeekType.TIMESTAMP, label: 'Timestamp' },
];
export const SeekDirectionOptions = [
{ value: SeekDirection.FORWARD, label: 'Oldest First', isLive: false },
{ value: SeekDirection.BACKWARD, label: 'Newest First', isLive: false },
{ value: SeekDirection.TAILING, label: 'Live Mode', isLive: true },
];
const Filters: React.FC<FiltersProps> = ({
clusterName,
@ -88,16 +85,14 @@ const Filters: React.FC<FiltersProps> = ({
const location = useLocation();
const history = useHistory();
const { searchParams, seekDirection, isLive, changeSeekDirection } =
useContext(TopicMessagesContext);
const [isOpen, setIsOpen] = React.useState(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
const source = React.useRef<EventSource | null>(null);
const searchParams = React.useMemo(
() => new URLSearchParams(location.search),
[location]
);
const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(
getSelectedPartitionsFromSeekToParam(searchParams, partitions)
);
@ -132,10 +127,6 @@ const Filters: React.FC<FiltersProps> = ({
: MessageFilterType.STRING_CONTAINS
);
const [query, setQuery] = React.useState<string>(searchParams.get('q') || '');
const [seekDirection, setSeekDirection] = React.useState<SeekDirection>(
(searchParams.get('seekDirection') as SeekDirection) ||
SeekDirection.FORWARD
);
const isSeekTypeControlVisible = React.useMemo(
() => selectedPartitions.length > 0,
[selectedPartitions]
@ -178,7 +169,7 @@ const Filters: React.FC<FiltersProps> = ({
setAttempt(attempt + 1);
if (isSeekTypeControlVisible) {
props.seekType = currentSeekType;
props.seekType = isLive ? SeekType.LATEST : currentSeekType;
props.seekTo = selectedPartitions.map(({ value }) => {
let seekToOffset;
@ -217,21 +208,6 @@ const Filters: React.FC<FiltersProps> = ({
query,
]);
const toggleSeekDirection = (val: string) => {
switch (val) {
case SeekDirection.FORWARD:
setSeekDirection(SeekDirection.FORWARD);
break;
case SeekDirection.BACKWARD:
setSeekDirection(SeekDirection.BACKWARD);
break;
case SeekDirection.TAILING:
setSeekDirection(SeekDirection.TAILING);
break;
default:
}
};
const handleSSECancel = () => {
if (!source.current) return;
@ -295,7 +271,7 @@ const Filters: React.FC<FiltersProps> = ({
};
// eslint-disable-next-line consistent-return
React.useEffect(() => {
if (location.search.length !== 0) {
if (location.search?.length !== 0) {
const url = `${BASE_PARAMS.basePath}/api/clusters/${clusterName}/topics/${topicName}/messages${location.search}`;
const sse = new EventSource(url);
@ -346,7 +322,7 @@ const Filters: React.FC<FiltersProps> = ({
updatePhase,
]);
React.useEffect(() => {
if (location.search.length === 0) {
if (location.search?.length === 0) {
handleFiltersSubmit();
}
}, [handleFiltersSubmit, location]);
@ -376,7 +352,7 @@ const Filters: React.FC<FiltersProps> = ({
selectSize="M"
minWidth="100px"
options={SeekTypeOptions}
disabled={seekDirection === SeekDirection.TAILING}
disabled={isLive}
/>
{currentSeekType === SeekType.OFFSET ? (
<Input
@ -387,7 +363,7 @@ const Filters: React.FC<FiltersProps> = ({
className="offset-selector"
placeholder="Offset"
onChange={({ target: { value } }) => setOffset(value)}
disabled={seekDirection === SeekDirection.TAILING}
disabled={isLive}
/>
) : (
<DatePicker
@ -398,7 +374,7 @@ const Filters: React.FC<FiltersProps> = ({
dateFormat="MMMM d, yyyy HH:mm"
className="date-picker"
placeholderText="Select timestamp"
disabled={seekDirection === SeekDirection.TAILING}
disabled={isLive}
/>
)}
</S.SeekTypeSelectorWrapper>
@ -440,11 +416,11 @@ const Filters: React.FC<FiltersProps> = ({
</S.FilterInputs>
<Select
selectSize="M"
onChange={(option) => toggleSeekDirection(option as string)}
onChange={(option) => changeSeekDirection(option as string)}
value={seekDirection}
minWidth="120px"
options={SeekDirectionOptions}
isLive={seekDirection === SeekDirection.TAILING}
isLive={isLive}
/>
</div>
<S.ActiveSmartFilterWrapper>
@ -479,12 +455,12 @@ const Filters: React.FC<FiltersProps> = ({
isFetching &&
phaseMessage}
</p>
<S.MessageLoading isLive={seekDirection === SeekDirection.TAILING}>
<S.MessageLoading isLive={isLive}>
<S.MessageLoadingSpinner isFetching={isFetching} />
Loading messages.
<S.StopLoading
onClick={() => {
setSeekDirection(SeekDirection.FORWARD);
changeSeekDirection(SeekDirection.FORWARD);
setIsFetching(false);
}}
>

View file

@ -1,29 +1,46 @@
import React from 'react';
import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
import Filters, {
FiltersProps,
SeekDirectionOptions,
SeekTypeOptions,
} from 'components/Topics/Topic/Details/Messages/Filters/Filters';
import { render } from 'lib/testHelpers';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TopicMessagesContext, {
ContextProps,
} from 'components/contexts/TopicMessagesContext';
import { SeekDirection } from 'generated-sources';
const setupWrapper = (props?: Partial<FiltersProps>) =>
const defaultContextValue: ContextProps = {
isLive: false,
seekDirection: SeekDirection.FORWARD,
searchParams: new URLSearchParams(''),
changeSeekDirection: jest.fn(),
};
const setupWrapper = (
props: Partial<FiltersProps> = {},
ctx: ContextProps = defaultContextValue
) => {
render(
<Filters
clusterName="test-cluster"
topicName="test-topic"
partitions={[{ partition: 0, offsetMin: 0, offsetMax: 100 }]}
meta={{}}
isFetching={false}
addMessage={jest.fn()}
resetMessages={jest.fn()}
updatePhase={jest.fn()}
updateMeta={jest.fn()}
setIsFetching={jest.fn()}
{...props}
/>
<TopicMessagesContext.Provider value={ctx}>
<Filters
clusterName="test-cluster"
topicName="test-topic"
partitions={[{ partition: 0, offsetMin: 0, offsetMax: 100 }]}
meta={{}}
isFetching={false}
addMessage={jest.fn()}
resetMessages={jest.fn()}
updatePhase={jest.fn()}
updateMeta={jest.fn()}
setIsFetching={jest.fn()}
{...props}
/>
</TopicMessagesContext.Provider>
);
};
describe('Filters component', () => {
it('renders component', () => {
setupWrapper();

View file

@ -1,13 +1,84 @@
import React from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
import { SeekDirection } from 'generated-sources';
import { useLocation } from 'react-router';
import FiltersContainer from './Filters/FiltersContainer';
import MessagesTable from './MessagesTable';
const Messages: React.FC = () => (
<div>
<FiltersContainer />
<MessagesTable />
</div>
);
export const SeekDirectionOptionsObj = {
[SeekDirection.FORWARD]: {
value: SeekDirection.FORWARD,
label: 'Oldest First',
isLive: false,
},
[SeekDirection.BACKWARD]: {
value: SeekDirection.BACKWARD,
label: 'Newest First',
isLive: false,
},
[SeekDirection.TAILING]: {
value: SeekDirection.TAILING,
label: 'Live Mode',
isLive: true,
},
};
export const SeekDirectionOptions = Object.values(SeekDirectionOptionsObj);
const Messages: React.FC = () => {
const location = useLocation();
const searchParams = React.useMemo(
() => new URLSearchParams(location.search),
[location.search]
);
const defaultSeekValue = SeekDirectionOptions[0];
const [seekDirection, setSeekDirection] = React.useState<SeekDirection>(
(searchParams.get('seekDirection') as SeekDirection) ||
defaultSeekValue.value
);
const [isLive, setIsLive] = useState<boolean>(
SeekDirectionOptionsObj[seekDirection].isLive
);
const changeSeekDirection = useCallback((val: string) => {
switch (val) {
case SeekDirection.FORWARD:
setSeekDirection(SeekDirection.FORWARD);
setIsLive(SeekDirectionOptionsObj[SeekDirection.FORWARD].isLive);
break;
case SeekDirection.BACKWARD:
setSeekDirection(SeekDirection.BACKWARD);
setIsLive(SeekDirectionOptionsObj[SeekDirection.BACKWARD].isLive);
break;
case SeekDirection.TAILING:
setSeekDirection(SeekDirection.TAILING);
setIsLive(SeekDirectionOptionsObj[SeekDirection.TAILING].isLive);
break;
default:
}
}, []);
const contextValue = useMemo(
() => ({
seekDirection,
searchParams,
changeSeekDirection,
isLive,
}),
[seekDirection, searchParams, changeSeekDirection]
);
return (
<TopicMessagesContext.Provider value={contextValue}>
<FiltersContainer />
<MessagesTable />
</TopicMessagesContext.Provider>
);
};
export default Messages;

View file

@ -4,13 +4,14 @@ import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeader
import { SeekDirection, TopicMessage } from 'generated-sources';
import styled from 'styled-components';
import { compact, concat, groupBy, map, maxBy, minBy } from 'lodash';
import React from 'react';
import React, { useContext } from 'react';
import { useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router';
import { useHistory } from 'react-router';
import {
getTopicMessges,
getIsTopicMessagesFetching,
} from 'redux/reducers/topicMessages/selectors';
import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
import Message from './Message';
import * as S from './MessageContent/MessageContent.styled';
@ -22,13 +23,9 @@ const MessagesPaginationWrapperStyled = styled.div`
`;
const MessagesTable: React.FC = () => {
const location = useLocation();
const history = useHistory();
const searchParams = React.useMemo(
() => new URLSearchParams(location.search),
[location]
);
const { searchParams, isLive } = useContext(TopicMessagesContext);
const messages = useSelector(getTopicMessges);
const isFetching = useSelector(getIsTopicMessagesFetching);
@ -94,7 +91,7 @@ const MessagesTable: React.FC = () => {
message={message}
/>
))}
{isFetching && (
{(isFetching || isLive) && !messages.length && (
<tr>
<td colSpan={10}>
<PageLoader />
@ -108,9 +105,13 @@ const MessagesTable: React.FC = () => {
)}
</tbody>
</Table>
<MessagesPaginationWrapperStyled>
<S.PaginationButton onClick={handleNextClick}>Next</S.PaginationButton>
</MessagesPaginationWrapperStyled>
{!isLive && (
<MessagesPaginationWrapperStyled>
<S.PaginationButton onClick={handleNextClick}>
Next
</S.PaginationButton>
</MessagesPaginationWrapperStyled>
)}
</>
);
};

View file

@ -0,0 +1,77 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { render, EventSourceMock } from 'lib/testHelpers';
import Messages, {
SeekDirectionOptions,
SeekDirectionOptionsObj,
} from 'components/Topics/Topic/Details/Messages/Messages';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { SeekDirection, SeekType } from 'generated-sources';
import userEvent from '@testing-library/user-event';
describe('Messages', () => {
const searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`;
const setUpComponent = (param: string = searchParams) => {
const history = createMemoryHistory();
history.push({
search: new URLSearchParams(param).toString(),
});
return render(
<Router history={history}>
<Messages />
</Router>
);
};
beforeEach(() => {
Object.defineProperty(window, 'EventSource', {
value: EventSourceMock,
});
});
describe('component rendering default behavior with the search params', () => {
beforeEach(() => {
setUpComponent();
});
it('should check default seekDirection if it actually take the value from the url', () => {
expect(screen.getByRole('listbox')).toHaveTextContent(
SeekDirectionOptionsObj[SeekDirection.FORWARD].label
);
});
it('should check the SeekDirection select changes', () => {
const seekDirectionSelect = screen.getByRole('listbox');
const seekDirectionOption = screen.getByRole('option');
expect(seekDirectionOption).toHaveTextContent(
SeekDirectionOptionsObj[SeekDirection.FORWARD].label
);
const labelValue1 = SeekDirectionOptions[1].label;
userEvent.click(seekDirectionSelect);
userEvent.selectOptions(seekDirectionSelect, [
SeekDirectionOptions[1].label,
]);
expect(seekDirectionOption).toHaveTextContent(labelValue1);
const labelValue0 = SeekDirectionOptions[0].label;
userEvent.click(seekDirectionSelect);
userEvent.selectOptions(seekDirectionSelect, [
SeekDirectionOptions[0].label,
]);
expect(seekDirectionOption).toHaveTextContent(labelValue0);
});
});
describe('Component rendering with custom Url search params', () => {
it('reacts to a change of seekDirection in the url which make the select pick up different value', () => {
setUpComponent(
searchParams.replace(SeekDirection.FORWARD, SeekDirection.BACKWARD)
);
expect(screen.getByRole('listbox')).toHaveTextContent(
SeekDirectionOptionsObj[SeekDirection.BACKWARD].label
);
});
});
});

View file

@ -0,0 +1,73 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { render } from 'lib/testHelpers';
import MessagesTable from 'components/Topics/Topic/Details/Messages/MessagesTable';
import { Router } from 'react-router';
import { createMemoryHistory } from 'history';
import { SeekDirection, SeekType } from 'generated-sources';
import userEvent from '@testing-library/user-event';
import TopicMessagesContext, {
ContextProps,
} from 'components/contexts/TopicMessagesContext';
describe('MessagesTable', () => {
const searchParams = new URLSearchParams(
`?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`
);
const contextValue: ContextProps = {
isLive: false,
seekDirection: SeekDirection.FORWARD,
searchParams,
changeSeekDirection: jest.fn(),
};
const setUpComponent = (
params: URLSearchParams = searchParams,
ctx: ContextProps = contextValue
) => {
const history = createMemoryHistory();
history.push({
search: params.toString(),
});
return render(
<Router history={history}>
<TopicMessagesContext.Provider value={ctx}>
<MessagesTable />
</TopicMessagesContext.Provider>
</Router>
);
};
describe('Default props Setup for MessagesTable component', () => {
beforeEach(() => {
setUpComponent();
});
it('should check the render', () => {
expect(screen.getByRole('table')).toBeInTheDocument();
});
it('should check the if no elements is rendered in the table', () => {
expect(screen.getByText(/No messages found/i)).toBeInTheDocument();
});
it('should check if next button exist and check the click after next click', () => {
const nextBtnElement = screen.getByText(/next/i);
expect(nextBtnElement).toBeInTheDocument();
userEvent.click(nextBtnElement);
expect(screen.getByText(/No messages found/i)).toBeInTheDocument();
});
});
describe('Custom Setup with different props value', () => {
it('should check if next click is gone during isLive Param', () => {
setUpComponent(searchParams, { ...contextValue, isLive: true });
expect(screen.queryByText(/next/i)).not.toBeInTheDocument();
});
it('should check the display of the loader element', () => {
setUpComponent(searchParams, { ...contextValue, isLive: true });
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,15 @@
import React from 'react';
import { SeekDirection } from 'generated-sources';
export interface ContextProps {
seekDirection: SeekDirection;
searchParams: URLSearchParams;
changeSeekDirection(val: string): void;
isLive: boolean;
}
const TopicMessagesContext = React.createContext<ContextProps>(
{} as ContextProps
);
export default TopicMessagesContext;

View file

@ -101,3 +101,23 @@ const customRender = (
};
export { customRender as render };
export class EventSourceMock {
url: string;
close: () => void;
open: () => void;
error: () => void;
onmessage: () => void;
constructor(url: string) {
this.url = url;
this.open = jest.fn();
this.error = jest.fn();
this.onmessage = jest.fn();
this.close = jest.fn();
}
}

View file

@ -19,7 +19,7 @@ const reducer = (state = initialState, action: Action): TopicMessagesState => {
case getType(actions.addTopicMessage): {
return {
...state,
messages: [...state.messages, action.payload],
messages: [action.payload, ...state.messages],
};
}
case getType(actions.resetTopicMessages):