Merge branch 'schema_show_page' of github.com:provectus/kafka-ui into schema_show_page

This commit is contained in:
Guzel Kafizova 2021-02-17 13:32:32 +03:00
commit f50800e10c
18 changed files with 1313 additions and 838 deletions

View file

@ -15,7 +15,10 @@
},
"ecmaVersion": 2018,
"sourceType": "module",
"project": ["./tsconfig.json", "./src/setupTests.ts"]
"project": [
"./tsconfig.json",
"./src/setupTests.ts"
]
},
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
@ -30,7 +33,8 @@
"@typescript-eslint/explicit-module-boundary-types": "off",
"jsx-a11y/label-has-associated-control": "off",
"import/prefer-default-export": "off",
"@typescript-eslint/no-explicit-any": "error"
"@typescript-eslint/no-explicit-any": "error",
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
},
"overrides": [
{

View file

@ -1,8 +0,0 @@
module.exports = {
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,6 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@types/react-datepicker": "^3.1.1",
"@types/uuid": "^8.3.0",
"bulma": "^0.9.2",
"bulma-switch": "^2.0.0",
"classnames": "^2.2.6",
@ -34,7 +32,7 @@
"*.{js,ts,jsx,tsx}": [
"eslint -c .eslintrc.json --fix",
"git add",
"jest --bail --findRelatedTests"
"npm test -- --bail --findRelatedTests --watchAll=false"
]
},
"scripts": {
@ -43,6 +41,7 @@
"lint": "eslint --ext .tsx,.ts src/",
"lint:fix": "eslint --ext .tsx,.ts src/ --fix",
"test": "react-scripts test",
"test:CI": "CI=true npm test --watchAll=false",
"eject": "react-scripts eject",
"tsc": "tsc"
},
@ -67,6 +66,7 @@
]
},
"devDependencies": {
"@jest/types": "^26.6.2",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.1.2",
@ -76,16 +76,19 @@
"@types/lodash": "^4.14.165",
"@types/node": "^12.19.8",
"@types/react": "^17.0.0",
"@types/react-datepicker": "^3.1.1",
"@types/react-dom": "^17.0.0",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/redux": "^3.6.0",
"@types/redux-thunk": "^2.1.0",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.4.1",
"dotenv": "^8.2.0",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.1",
"eslint": "^7.14.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-typescript": "^12.0.0",
@ -100,12 +103,18 @@
"lint-staged": "^10.5.2",
"node-sass": "^4.14.1",
"prettier": "^2.2.1",
"react-scripts": "4.0.1",
"react-scripts": "4.0.2",
"ts-jest": "^26.4.4",
"ts-node": "^9.1.1",
"typescript": "~4.1.2"
},
"engines": {
"node": ">=14.15.4"
},
"proxy": "http://localhost:8080"
"proxy": "http://localhost:8080",
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
}
}

View file

@ -3,11 +3,11 @@ import { format } from 'date-fns';
import JSONTree from 'react-json-tree';
import { TopicMessage } from 'generated-sources';
interface MessageItemProp {
export interface MessageItemProp {
partition: TopicMessage['partition'];
offset: TopicMessage['offset'];
timestamp: TopicMessage['timestamp'];
content: TopicMessage['content'];
content?: TopicMessage['content'];
}
const MessageItem: React.FC<MessageItemProp> = ({
@ -16,13 +16,11 @@ const MessageItem: React.FC<MessageItemProp> = ({
timestamp,
content,
}) => (
<tr key="{timestamp}">
<td style={{ width: 200 }}>
{timestamp ? format(timestamp, 'yyyy-MM-dd HH:mm:ss') : null}
</td>
<tr>
<td style={{ width: 200 }}>{format(timestamp, 'yyyy-MM-dd HH:mm:ss')}</td>
<td style={{ width: 150 }}>{offset}</td>
<td style={{ width: 100 }}>{partition}</td>
<td key="{content}" style={{ wordBreak: 'break-word' }}>
<td style={{ wordBreak: 'break-word' }}>
{content && (
<JSONTree
data={content}

View file

@ -1,4 +1,9 @@
import 'react-datepicker/dist/react-datepicker.css';
import React, { useCallback, useEffect, useRef } from 'react';
import { groupBy, map, concat, maxBy } from 'lodash';
import MultiSelect from 'react-multi-select-component';
import { Option } from 'react-multi-select-component/dist/lib/interfaces';
import { useDebouncedCallback } from 'use-debounce';
import {
ClusterName,
TopicMessageQueryParams,
@ -7,13 +12,6 @@ import {
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 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 {
@ -81,17 +79,17 @@ const Messages: React.FC<Props> = ({
offset: 0,
partition: p.partition,
}));
const messageUniqs: FilterProps[] = _.map(
_.groupBy(messages, 'partition'),
(v) => _.maxBy(v, 'offset')
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
return map(
groupBy(concat(partitionUniqs, messageUniqs), 'partition'),
(v) => maxBy(v, 'offset') as FilterProps
);
}, [messages, partitions]);

View file

@ -1,10 +1,5 @@
import { connect } from 'react-redux';
import {
ClusterName,
RootState,
TopicMessageQueryParams,
TopicName,
} from 'redux/interfaces';
import { ClusterName, RootState, TopicName } from 'redux/interfaces';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { fetchTopicMessages } from 'redux/actions';
import {
@ -38,11 +33,7 @@ const mapStateToProps = (
});
const mapDispatchToProps = {
fetchTopicMessages: (
clusterName: ClusterName,
topicName: TopicName,
queryParams: Partial<TopicMessageQueryParams>
) => fetchTopicMessages(clusterName, topicName, queryParams),
fetchTopicMessages,
};
export default withRouter(

View file

@ -3,7 +3,7 @@ import { TopicMessage } from 'generated-sources';
import CustomParamButton from 'components/Topics/shared/Form/CustomParams/CustomParamButton';
import MessageItem from './MessageItem';
interface MessagesTableProp {
export interface MessagesTableProp {
messages: TopicMessage[];
onNext(event: React.MouseEvent<HTMLButtonElement>): void;
}
@ -14,7 +14,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
}
return (
<div>
<>
<table className="table is-striped is-fullwidth">
<thead>
<tr>
@ -28,7 +28,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
{messages.map(
({ partition, offset, timestamp, content }: TopicMessage) => (
<MessageItem
key={timestamp.toString()}
key={`message-${timestamp.getTime()}`}
partition={partition}
offset={offset}
timestamp={timestamp}
@ -48,7 +48,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
/>
</div>
</div>
</div>
</>
);
};

View file

@ -0,0 +1,38 @@
import React from 'react';
import { shallow } from 'enzyme';
import MessageItem from 'components/Topics/Details/Messages/MessageItem';
import { messages } from './fixtures';
jest.mock('date-fns', () => ({
format: () => `mocked date`,
}));
describe('MessageItem', () => {
describe('when content is defined', () => {
it('renders table row with JSONTree', () => {
const wrapper = shallow(<MessageItem {...messages[0]} />);
expect(wrapper.find('tr').length).toEqual(1);
expect(wrapper.find('td').length).toEqual(4);
expect(wrapper.find('JSONTree').length).toEqual(1);
});
it('matches snapshot', () => {
expect(shallow(<MessageItem {...messages[0]} />)).toMatchSnapshot();
});
});
describe('when content is undefined', () => {
it('renders table row without JSONTree', () => {
const wrapper = shallow(<MessageItem {...messages[1]} />);
expect(wrapper.find('tr').length).toEqual(1);
expect(wrapper.find('td').length).toEqual(4);
expect(wrapper.find('JSONTree').length).toEqual(0);
});
it('matches snapshot', () => {
expect(shallow(<MessageItem {...messages[1]} />)).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,178 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mount, shallow } from 'enzyme';
import * as useDebounce from 'use-debounce';
import DatePicker from 'react-datepicker';
import Messages, { Props } from 'components/Topics/Details/Messages/Messages';
import MessagesContainer from 'components/Topics/Details/Messages/MessagesContainer';
import PageLoader from 'components/common/PageLoader/PageLoader';
import configureStore from 'redux/store/configureStore';
describe('Messages', () => {
describe('Container', () => {
const store = configureStore();
it('renders view', () => {
const component = shallow(
<Provider store={store}>
<MessagesContainer />
</Provider>
);
expect(component.exists()).toBeTruthy();
});
});
describe('View', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
const setupWrapper = (props: Partial<Props> = {}) => (
<Messages
clusterName="Test cluster"
topicName="Cluster topic"
isFetched
fetchTopicMessages={jest.fn()}
messages={[]}
partitions={[]}
{...props}
/>
);
describe('Initial state', () => {
it('renders PageLoader', () => {
expect(
shallow(setupWrapper({ isFetched: false })).exists(PageLoader)
).toBeTruthy();
});
});
describe('Table', () => {
describe('With messages', () => {
const messagesWrapper = mount(
setupWrapper({
messages: [
{
partition: 1,
offset: 2,
timestamp: new Date('05-05-1994'),
content: [1, 2, 3],
},
],
})
);
it('renders table', () => {
expect(
messagesWrapper.exists(
'[className="table is-striped is-fullwidth"]'
)
).toBeTruthy();
});
it('renders JSONTree', () => {
expect(messagesWrapper.find('JSONTree').length).toEqual(1);
});
it('parses message content correctly', () => {
const messages = [
{
partition: 1,
offset: 2,
timestamp: new Date('05-05-1994'),
content: [1, 2, 3],
},
];
const content = JSON.stringify(messages[0].content);
expect(JSON.parse(content)).toEqual(messages[0].content);
});
});
describe('Without messages', () => {
it('renders string', () => {
const wrapper = mount(setupWrapper());
expect(wrapper.text()).toContain('No messages at selected topic');
});
});
});
describe('Offset field', () => {
describe('Seek Type dependency', () => {
const wrapper = mount(setupWrapper());
it('renders DatePicker', () => {
wrapper
.find('[id="selectSeekType"]')
.simulate('change', { target: { value: 'TIMESTAMP' } });
expect(
wrapper.find('[id="selectSeekType"]').first().props().value
).toEqual('TIMESTAMP');
expect(wrapper.exists(DatePicker)).toBeTruthy();
});
});
describe('With defined offset value', () => {
const wrapper = shallow(setupWrapper());
it('shows offset value in input', () => {
const offset = '10';
wrapper
.find('#searchOffset')
.simulate('change', { target: { value: offset } });
expect(wrapper.find('#searchOffset').first().props().value).toEqual(
offset
);
});
});
describe('With invalid offset value', () => {
const wrapper = shallow(setupWrapper());
it('shows 0 in input', () => {
wrapper
.find('#searchOffset')
.simulate('change', { target: { value: null } });
expect(wrapper.find('#searchOffset').first().props().value).toBe('0');
});
});
});
describe('Search field', () => {
it('renders input correctly', () => {
const query = 20;
const mockedUseDebouncedCallback = jest.fn();
jest
.spyOn(useDebounce, 'useDebouncedCallback')
.mockImplementationOnce(() => [
mockedUseDebouncedCallback,
jest.fn(),
jest.fn(),
]);
const wrapper = shallow(setupWrapper());
wrapper
.find('#searchText')
.simulate('change', { target: { value: query } });
expect(wrapper.find('#searchText').first().props().value).toEqual(
query
);
expect(mockedUseDebouncedCallback).toHaveBeenCalledWith({ q: query });
});
});
describe('Submit button', () => {
it('fetches topic messages', () => {
const mockedfetchTopicMessages = jest.fn();
const wrapper = mount(
setupWrapper({ fetchTopicMessages: mockedfetchTopicMessages })
);
wrapper.find('[type="submit"]').simulate('click');
expect(mockedfetchTopicMessages).toHaveBeenCalled();
});
});
});
});

View file

@ -0,0 +1,49 @@
import React from 'react';
import { shallow } from 'enzyme';
import MessagesTable, {
MessagesTableProp,
} from 'components/Topics/Details/Messages/MessagesTable';
import { messages } from './fixtures';
jest.mock('date-fns', () => ({
format: () => `mocked date`,
}));
describe('MessagesTable', () => {
const setupWrapper = (props: Partial<MessagesTableProp> = {}) => (
<MessagesTable messages={[]} onNext={jest.fn()} {...props} />
);
describe('when topic is empty', () => {
it('renders table row with JSONTree', () => {
const wrapper = shallow(setupWrapper());
expect(wrapper.exists('table')).toBeFalsy();
expect(wrapper.exists('CustomParamButton')).toBeFalsy();
expect(wrapper.text()).toEqual('No messages at selected topic');
});
it('matches snapshot', () => {
expect(shallow(setupWrapper())).toMatchSnapshot();
});
});
describe('when topic contains messages', () => {
const onNext = jest.fn();
const wrapper = shallow(setupWrapper({ messages, onNext }));
it('renders table row without JSONTree', () => {
expect(wrapper.exists('table')).toBeTruthy();
expect(wrapper.exists('CustomParamButton')).toBeTruthy();
expect(wrapper.find('MessageItem').length).toEqual(2);
});
it('handles CustomParamButton click', () => {
wrapper.find('CustomParamButton').simulate('click');
expect(onNext).toHaveBeenCalled();
});
it('matches snapshot', () => {
expect(shallow(setupWrapper({ messages, onNext }))).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,110 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MessageItem when content is defined matches snapshot 1`] = `
<tr>
<td
style={
Object {
"width": 200,
}
}
>
mocked date
</td>
<td
style={
Object {
"width": 150,
}
}
>
2
</td>
<td
style={
Object {
"width": 100,
}
}
>
1
</td>
<td
style={
Object {
"wordBreak": "break-word",
}
}
>
<JSONTree
collectionLimit={50}
data={
Object {
"foo": "bar",
"key": "val",
}
}
getItemString={[Function]}
hideRoot={true}
invertTheme={false}
isCustomNode={[Function]}
keyPath={
Array [
"root",
]
}
labelRenderer={[Function]}
postprocessValue={[Function]}
shouldExpandNode={[Function]}
theme={
Object {
"base0B": "#363636",
"base0D": "#3273dc",
"tree": [Function],
"value": [Function],
}
}
valueRenderer={[Function]}
/>
</td>
</tr>
`;
exports[`MessageItem when content is undefined matches snapshot 1`] = `
<tr>
<td
style={
Object {
"width": 200,
}
}
>
mocked date
</td>
<td
style={
Object {
"width": 150,
}
}
>
20
</td>
<td
style={
Object {
"width": 100,
}
}
>
2
</td>
<td
style={
Object {
"wordBreak": "break-word",
}
}
/>
</tr>
`;

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MessagesTable when topic contains messages matches snapshot 1`] = `
<Fragment>
<table
className="table is-striped is-fullwidth"
>
<thead>
<tr>
<th>
Timestamp
</th>
<th>
Offset
</th>
<th>
Partition
</th>
<th>
Content
</th>
</tr>
</thead>
<tbody>
<MessageItem
content={
Object {
"foo": "bar",
"key": "val",
}
}
key="message-802310400000"
offset={2}
partition={1}
timestamp={1995-06-05T00:00:00.000Z}
/>
<MessageItem
key="message-1596585600000"
offset={20}
partition={2}
timestamp={2020-08-05T00:00:00.000Z}
/>
</tbody>
</table>
<div
className="columns"
>
<div
className="column is-full"
>
<CustomParamButton
btnText="Next"
className="is-link is-pulled-right"
onClick={[MockFunction]}
type="fa-chevron-right"
/>
</div>
</div>
</Fragment>
`;
exports[`MessagesTable when topic is empty matches snapshot 1`] = `
<div>
No messages at selected topic
</div>
`;

View file

@ -0,0 +1,19 @@
import { TopicMessage } from 'generated-sources';
export const messages: TopicMessage[] = [
{
partition: 1,
offset: 2,
timestamp: new Date(Date.UTC(1995, 5, 5)),
content: {
foo: 'bar',
key: 'val',
},
},
{
partition: 2,
offset: 20,
timestamp: new Date(Date.UTC(2020, 7, 5)),
content: undefined,
},
];

View file

@ -88,7 +88,7 @@ const CustomParams: React.FC<Props> = ({ isSubmitting, config }) => {
{formCustomParams.allIndexes.map((index) => (
<CustomParamField
key={index}
key={formCustomParams.byIndex[index].name}
index={index}
isDisabled={isSubmitting}
name={formCustomParams.byIndex[index].name}

View file

@ -1,6 +1,5 @@
/* eslint-disable */
import * as Enzyme from 'enzyme';
import { configure } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import '@testing-library/jest-dom/extend-expect';
Enzyme.configure({ adapter: new Adapter() });
configure({ adapter: new Adapter() });

View file

@ -1,156 +0,0 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import JSONTree from 'react-json-tree';
import * as useDebounce from 'use-debounce';
import DatePicker from 'react-datepicker';
import Messages, { Props } from 'components/Topics/Details/Messages/Messages';
import PageLoader from 'components/common/PageLoader/PageLoader';
describe('Messages component', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
const setupWrapper = (props: Partial<Props> = {}) => (
<Messages
clusterName="Test cluster"
topicName="Cluster topic"
isFetched
fetchTopicMessages={jest.fn()}
messages={[]}
partitions={[]}
{...props}
/>
);
describe('Initial state', () => {
it('renders PageLoader', () => {
expect(
shallow(setupWrapper({ isFetched: false })).exists(PageLoader)
).toBeTruthy();
});
});
describe('Messages table', () => {
describe('With messages', () => {
const messagesWrapper = mount(
setupWrapper({
messages: [
{
partition: 1,
offset: 2,
timestamp: new Date('05-05-1994'),
content: [1, 2, 3],
},
],
})
);
it('renders table', () => {
expect(
messagesWrapper.exists('[className="table is-striped is-fullwidth"]')
).toBeTruthy();
});
it('renders JSONTree', () => {
expect(messagesWrapper.find(JSONTree).length).toEqual(1);
});
it('parses message content correctly', () => {
const messages = [
{
partition: 1,
offset: 2,
timestamp: new Date('05-05-1994'),
content: [1, 2, 3],
},
];
const content = JSON.stringify(messages[0].content);
expect(JSON.parse(content)).toEqual(messages[0].content);
});
});
describe('Without messages', () => {
it('renders string', () => {
const wrapper = mount(setupWrapper());
expect(wrapper.text()).toContain('No messages at selected topic');
});
});
});
describe('Offset field', () => {
describe('Seek Type dependency', () => {
const wrapper = mount(setupWrapper());
it('renders DatePicker', () => {
wrapper
.find('[id="selectSeekType"]')
.simulate('change', { target: { value: 'TIMESTAMP' } });
expect(
wrapper.find('[id="selectSeekType"]').first().props().value
).toEqual('TIMESTAMP');
expect(wrapper.exists(DatePicker)).toBeTruthy();
});
});
describe('With defined offset value', () => {
const wrapper = shallow(setupWrapper());
it('shows offset value in input', () => {
const offset = '10';
wrapper
.find('#searchOffset')
.simulate('change', { target: { value: offset } });
expect(wrapper.find('#searchOffset').first().props().value).toEqual(
offset
);
});
});
describe('With invalid offset value', () => {
const wrapper = shallow(setupWrapper());
it('shows 0 in input', () => {
wrapper
.find('#searchOffset')
.simulate('change', { target: { value: null } });
expect(wrapper.find('#searchOffset').first().props().value).toBe('0');
});
});
});
describe('Search field', () => {
it('renders input correctly', () => {
const query = 20;
const mockedUseDebouncedCallback = jest.fn();
jest
.spyOn(useDebounce, 'useDebouncedCallback')
.mockImplementationOnce(() => [
mockedUseDebouncedCallback,
jest.fn(),
jest.fn(),
]);
const wrapper = shallow(setupWrapper());
wrapper
.find('#searchText')
.simulate('change', { target: { value: query } });
expect(wrapper.find('#searchText').first().props().value).toEqual(query);
expect(mockedUseDebouncedCallback).toHaveBeenCalledWith({ q: query });
});
});
describe('Submit button', () => {
it('fetches topic messages', () => {
const mockedfetchTopicMessages = jest.fn();
const wrapper = mount(
setupWrapper({ fetchTopicMessages: mockedfetchTopicMessages })
);
wrapper.find('[type="submit"]').simulate('click');
expect(mockedfetchTopicMessages).toHaveBeenCalled();
});
});
});

View file

@ -22,6 +22,6 @@
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
"src",
]
}