Merge branch 'master' into crud-schema-registry
This commit is contained in:
commit
68d9112b77
15 changed files with 2774 additions and 4459 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
v12.13.1
|
||||
v14.15.4
|
||||
|
|
8
kafka-ui-react-app/jest.config.js
Normal file
8
kafka-ui-react-app/jest.config.js
Normal 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'],
|
||||
};
|
6985
kafka-ui-react-app/package-lock.json
generated
6985
kafka-ui-react-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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' ||
|
||||
|
|
|
@ -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() });
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
4
pom.xml
4
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue