Merge branch 'master' into crud-schema-registry

This commit is contained in:
Ildar Almakaev 2021-02-04 15:38:51 +03:00
commit 68d9112b77
15 changed files with 2774 additions and 4459 deletions

View file

@ -33,12 +33,13 @@ The official Docker image for Kafka UI is hosted here: [hub.docker.com/r/provect
Launch Docker container in the background:
```sh
docker run -d provectuslabs/kafka-ui:latest
docker run -p 8080:8080
-e KAFKA_CLUSTERS_0_NAME=local
-e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
-d provectuslabs/kafka-ui:latest
```
Then access the web UI at [http://localhost:9000](http://localhost:9000).
Then access the web UI at [http://localhost:8080](http://localhost:8080).
## Building With Docker

View file

@ -5,7 +5,7 @@
<parent>
<artifactId>kafka-ui</artifactId>
<groupId>com.provectus</groupId>
<version>0.0.9-SNAPSHOT</version>
<version>0.0.10-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View file

@ -68,6 +68,7 @@ public class ClusterService {
.map(c ->
c.getTopics().values().stream()
.map(clusterMapper::toTopic)
.sorted(Comparator.comparing(Topic::getName))
.collect(Collectors.toList())
).orElse(Collections.emptyList());
}

View file

@ -4,7 +4,7 @@
<parent>
<artifactId>kafka-ui</artifactId>
<groupId>com.provectus</groupId>
<version>0.0.9-SNAPSHOT</version>
<version>0.0.10-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View file

@ -1,7 +1,8 @@
{
"env": {
"browser": true,
"es6": true
"es6": true,
"jest": true
},
"globals": {
"Atomics": "readonly",
@ -14,7 +15,7 @@
},
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
"project": ["./tsconfig.json", "./src/setupTests.ts"]
},
"plugins": ["@typescript-eslint", "prettier"],
"extends": [
@ -28,7 +29,8 @@
"prettier/prettier": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"jsx-a11y/label-has-associated-control": "off",
"import/prefer-default-export": "off"
"import/prefer-default-export": "off",
"@typescript-eslint/no-explicit-any": "error"
},
"overrides": [
{
@ -36,6 +38,12 @@
"rules": {
"react/prop-types": "off"
}
},
{
"files": ["*.spec.tsx"],
"rules": {
"react/jsx-props-no-spreading": "off"
}
}
]
}

View file

@ -1 +1 @@
v12.13.1
v14.15.4

View file

@ -0,0 +1,8 @@
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

@ -32,7 +32,8 @@
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint -c .eslintrc.json --fix",
"git add"
"git add",
"jest --bail --findRelatedTests"
]
},
"scripts": {
@ -65,11 +66,12 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.1.2",
"@types/classnames": "^2.2.11",
"@types/jest": "^24.9.1",
"@types/enzyme": "^3.10.8",
"@types/jest": "^26.0.20",
"@types/lodash": "^4.14.165",
"@types/node": "^12.19.8",
"@types/react": "^17.0.0",
@ -80,7 +82,9 @@
"@types/redux-thunk": "^2.1.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",
"eslint": "^7.14.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-typescript": "^12.0.0",
@ -96,7 +100,11 @@
"node-sass": "^4.14.1",
"prettier": "^2.2.1",
"react-scripts": "4.0.1",
"ts-jest": "^26.4.4",
"typescript": "~4.1.2"
},
"engines": {
"node": ">=14.15.4"
},
"proxy": "http://localhost:8080"
}

View file

@ -20,7 +20,7 @@ import * as _ from 'lodash';
import { useDebouncedCallback } from 'use-debounce';
import { Option } from 'react-multi-select-component/dist/lib/interfaces';
interface Props {
export interface Props {
clusterName: ClusterName;
topicName: TopicName;
isFetched: boolean;
@ -38,8 +38,8 @@ interface FilterProps {
partition: TopicMessage['partition'];
}
function usePrevious(value: any) {
const ref = useRef();
function usePrevious(value: Date | null) {
const ref = useRef<Date | null>();
useEffect(() => {
ref.current = value;
});
@ -73,7 +73,8 @@ const Messages: React.FC<Props> = ({
Partial<TopicMessageQueryParams>
>({ limit: 100 });
const [debouncedCallback] = useDebouncedCallback(
(query: any) => setQueryParams({ ...queryParams, ...query }),
(query: Partial<TopicMessageQueryParams>) =>
setQueryParams({ ...queryParams, ...query }),
1000
);
@ -98,7 +99,7 @@ const Messages: React.FC<Props> = ({
);
}, [messages, partitions]);
const getSeekToValuesForPartitions = (partition: any) => {
const getSeekToValuesForPartitions = (partition: Option) => {
const foundedValues = filterProps.find(
(prop) => prop.partition === partition.value
);
@ -178,7 +179,7 @@ const Messages: React.FC<Props> = ({
return format(date, 'yyyy-MM-dd HH:mm:ss');
};
const getMessageContentBody = (content: any) => {
const getMessageContentBody = (content: Record<string, unknown>) => {
try {
const contentObj =
typeof content !== 'object' ? JSON.parse(content) : content;
@ -226,7 +227,7 @@ const Messages: React.FC<Props> = ({
});
};
const filterOptions = (options: Option[], filter: any) => {
const filterOptions = (options: Option[], filter: string) => {
if (!filter) {
return options;
}
@ -256,7 +257,10 @@ const Messages: React.FC<Props> = ({
<td style={{ width: 150 }}>{message.offset}</td>
<td style={{ width: 100 }}>{message.partition}</td>
<td key={Math.random()} style={{ wordBreak: 'break-word' }}>
{getMessageContentBody(message.content)}
{message.content &&
getMessageContentBody(
message.content as Record<string, unknown>
)}
</td>
</tr>
))}
@ -305,7 +309,6 @@ const Messages: React.FC<Props> = ({
id="selectSeekType"
name="selectSeekType"
onChange={handleSeekTypeChange}
defaultValue={SeekType.OFFSET}
value={selectedSeekType}
>
<option value={SeekType.OFFSET}>Offset</option>

View file

@ -2,11 +2,17 @@ import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../../reducers';
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
}
}
export default () => {
const middlewares = [thunk];
const composeEnhancers =
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const enhancer = composeEnhancers(applyMiddleware(...middlewares));

View file

@ -9,6 +9,7 @@
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
/* eslint-disable no-console */
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||

View file

@ -1,6 +1,6 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
// eslint-disable-next-line import/no-extraneous-dependencies
/* eslint-disable */
import * as Enzyme from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import '@testing-library/jest-dom/extend-expect';
Enzyme.configure({ adapter: new Adapter() });

View file

@ -0,0 +1,156 @@
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

@ -20,7 +20,7 @@
<git.revision>latest</git.revision>
<zkclient.version>0.11</zkclient.version>
<kafka-clients.version>2.4.0</kafka-clients.version>
<node.version>v12.13.1</node.version>
<node.version>v14.15.4</node.version>
<dockerfile-maven-plugin.version>1.4.10</dockerfile-maven-plugin.version>
<frontend-maven-plugin.version>1.8.0</frontend-maven-plugin.version>
<maven-compiler-plugin.version>3.5.1</maven-compiler-plugin.version>
@ -51,7 +51,7 @@
<groupId>com.provectus</groupId>
<artifactId>kafka-ui</artifactId>
<version>0.0.9-SNAPSHOT</version>
<version>0.0.10-SNAPSHOT</version>
<name>kafka-ui</name>
<description>Kafka metrics for UI panel</description>
</project>