Bläddra i källkod

Merge branch 'master' into crud-schema-registry

Ildar Almakaev 4 år sedan
förälder
incheckning
68d9112b77

+ 3 - 2
README.md

@@ -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:
 Launch Docker container in the background:
 ```sh
 ```sh
 
 
-docker run -d provectuslabs/kafka-ui:latest 
+docker run -p 8080:8080
 	-e KAFKA_CLUSTERS_0_NAME=local
 	-e KAFKA_CLUSTERS_0_NAME=local
 	-e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
 	-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
 ## Building With Docker

+ 1 - 1
kafka-ui-api/pom.xml

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

+ 1 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/service/ClusterService.java

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

+ 1 - 1
kafka-ui-contract/pom.xml

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

+ 11 - 3
kafka-ui-react-app/.eslintrc.json

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

+ 1 - 1
kafka-ui-react-app/.nvmrc

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

+ 8 - 0
kafka-ui-react-app/jest.config.js

@@ -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'],
+};

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 144 - 684
kafka-ui-react-app/package-lock.json


+ 11 - 3
kafka-ui-react-app/package.json

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

+ 12 - 9
kafka-ui-react-app/src/components/Topics/Details/Messages/Messages.tsx

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

+ 7 - 1
kafka-ui-react-app/src/redux/store/configureStore/dev.ts

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

+ 1 - 0
kafka-ui-react-app/src/serviceWorker.ts

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

+ 5 - 5
kafka-ui-react-app/src/setupTests.ts

@@ -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';
 import '@testing-library/jest-dom/extend-expect';
+
+Enzyme.configure({ adapter: new Adapter() });

+ 156 - 0
kafka-ui-react-app/src/tests/Topics/Details/Messages/Messages.spec.tsx

@@ -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();
+    });
+  });
+});

+ 2 - 2
pom.xml

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

Vissa filer visades inte eftersom för många filer har ändrats