Sfoglia il codice sorgente

Sending messages (#675)

* Implement message sending
Alexander Krivonosov 3 anni fa
parent
commit
ad8598a1d2

+ 336 - 34
kafka-ui-react-app/package-lock.json

@@ -1450,6 +1450,18 @@
         "strip-json-comments": "^3.1.1"
       },
       "dependencies": {
+        "ajv": {
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
         "debug": {
           "version": "4.3.2",
           "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
@@ -1465,6 +1477,12 @@
           "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
           "dev": true
         },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -3092,6 +3110,11 @@
       "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz",
       "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
     },
+    "@types/yup": {
+      "version": "0.29.13",
+      "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.13.tgz",
+      "integrity": "sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g=="
+    },
     "@typescript-eslint/eslint-plugin": {
       "version": "4.28.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz",
@@ -3689,18 +3712,6 @@
         "indent-string": "^4.0.0"
       }
     },
-    "ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
-      "requires": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      }
-    },
     "ajv-errors": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
@@ -7527,6 +7538,18 @@
             "@babel/highlight": "^7.10.4"
           }
         },
+        "ajv": {
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
         "debug": {
           "version": "4.3.2",
           "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
@@ -7565,6 +7588,12 @@
           "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
           "dev": true
         },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -7962,6 +7991,12 @@
         "schema-utils": "^3.0.0"
       },
       "dependencies": {
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "normalize-path": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -7977,6 +8012,20 @@
             "@types/json-schema": "^7.0.6",
             "ajv": "^6.12.5",
             "ajv-keywords": "^3.5.2"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         }
       }
@@ -8478,6 +8527,11 @@
       "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=",
       "dev": true
     },
+    "faker": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz",
+      "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g=="
+    },
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -8633,6 +8687,12 @@
         "schema-utils": "^3.0.0"
       },
       "dependencies": {
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "schema-utils": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
@@ -8642,6 +8702,20 @@
             "@types/json-schema": "^7.0.6",
             "ajv": "^6.12.5",
             "ajv-keywords": "^3.5.2"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         }
       }
@@ -8827,6 +8901,11 @@
         }
       }
     },
+    "fn-name": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz",
+      "integrity": "sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA=="
+    },
     "follow-redirects": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
@@ -9279,8 +9358,7 @@
     "get-own-enumerable-property-symbols": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
-      "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
-      "dev": true
+      "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="
     },
     "get-package-type": {
       "version": "0.1.0",
@@ -9470,6 +9548,26 @@
       "requires": {
         "ajv": "^6.12.3",
         "har-schema": "^2.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        }
       }
     },
     "harmony-reflect": {
@@ -10576,8 +10674,7 @@
     "is-obj": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
-      "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
-      "dev": true
+      "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
     },
     "is-path-cwd": {
       "version": "2.2.0",
@@ -10637,8 +10734,22 @@
     "is-regexp": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
-      "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
-      "dev": true
+      "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk="
+    },
+    "is-relative-url": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-relative-url/-/is-relative-url-3.0.0.tgz",
+      "integrity": "sha512-U1iSYRlY2GIMGuZx7gezlB5dp1Kheaym7zKzO1PV06mOihiWTXejLwm4poEJysPyXF+HtK/BEd0DVlcCh30pEA==",
+      "requires": {
+        "is-absolute-url": "^3.0.0"
+      },
+      "dependencies": {
+        "is-absolute-url": {
+          "version": "3.0.3",
+          "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
+          "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q=="
+        }
+      }
     },
     "is-resolvable": {
       "version": "1.1.0",
@@ -12437,11 +12548,32 @@
       "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
       "dev": true
     },
-    "json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
+    "json-schema-yup-transformer": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/json-schema-yup-transformer/-/json-schema-yup-transformer-1.6.0.tgz",
+      "integrity": "sha512-fxS3g2t+taJ8ZBlbmA8rK3YO9SyjeQxvCIIJxC8iDsaqnGn0SVI3DnFePdmtHmYmyor9mlKOKvk6Sc0YT2ZP7Q==",
+      "requires": {
+        "is-relative-url": "3.0.0",
+        "lodash": "4.17.21",
+        "stringify-object": "^3.3.0",
+        "yup": "^0.29.1"
+      },
+      "dependencies": {
+        "yup": {
+          "version": "0.29.3",
+          "resolved": "https://registry.npmjs.org/yup/-/yup-0.29.3.tgz",
+          "integrity": "sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==",
+          "requires": {
+            "@babel/runtime": "^7.10.5",
+            "fn-name": "~3.0.0",
+            "lodash": "^4.17.15",
+            "lodash-es": "^4.17.11",
+            "property-expr": "^2.0.2",
+            "synchronous-promise": "^2.0.13",
+            "toposort": "^2.0.2"
+          }
+        }
+      }
     },
     "json-stable-stringify-without-jsonify": {
       "version": "1.0.1",
@@ -13338,6 +13470,12 @@
         "webpack-sources": "^1.1.0"
       },
       "dependencies": {
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "loader-utils": {
           "version": "1.4.0",
           "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
@@ -13358,6 +13496,20 @@
             "ajv": "^6.1.0",
             "ajv-errors": "^1.0.0",
             "ajv-keywords": "^3.1.0"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         }
       }
@@ -13607,6 +13759,16 @@
           "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
           "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
           "dev": true
+        },
+        "randexp": {
+          "version": "0.4.6",
+          "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
+          "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
+          "dev": true,
+          "requires": {
+            "discontinuous-range": "1.0.0",
+            "ret": "~0.1.10"
+          }
         }
       }
     },
@@ -15113,6 +15275,12 @@
         "schema-utils": "^1.0.0"
       },
       "dependencies": {
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "loader-utils": {
           "version": "1.4.0",
           "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
@@ -15133,6 +15301,20 @@
             "ajv": "^6.1.0",
             "ajv-errors": "^1.0.0",
             "ajv-keywords": "^3.1.0"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         }
       }
@@ -16066,16 +16248,6 @@
       "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=",
       "dev": true
     },
-    "randexp": {
-      "version": "0.4.6",
-      "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
-      "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
-      "dev": true,
-      "requires": {
-        "discontinuous-range": "1.0.0",
-        "ret": "~0.1.10"
-      }
-    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -17777,6 +17949,12 @@
         "semver": "^7.3.2"
       },
       "dependencies": {
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "schema-utils": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
@@ -17786,6 +17964,20 @@
             "@types/json-schema": "^7.0.6",
             "ajv": "^6.12.5",
             "ajv-keywords": "^3.5.2"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         },
         "semver": {
@@ -17832,6 +18024,26 @@
         "@types/json-schema": "^7.0.5",
         "ajv": "^6.12.4",
         "ajv-keywords": "^3.5.2"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        }
       }
     },
     "scss-tokenizer": {
@@ -18792,7 +19004,6 @@
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
       "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
-      "dev": true,
       "requires": {
         "get-own-enumerable-property-symbols": "^3.0.0",
         "is-obj": "^1.0.1",
@@ -19033,6 +19244,11 @@
       "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
       "dev": true
     },
+    "synchronous-promise": {
+      "version": "2.0.15",
+      "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.15.tgz",
+      "integrity": "sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg=="
+    },
     "table": {
       "version": "6.7.1",
       "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz",
@@ -19193,6 +19409,12 @@
             "path-exists": "^4.0.0"
           }
         },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "locate-path": {
           "version": "5.0.0",
           "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -19270,6 +19492,20 @@
             "@types/json-schema": "^7.0.6",
             "ajv": "^6.12.5",
             "ajv-keywords": "^3.5.2"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         },
         "semver": {
@@ -19921,6 +20157,12 @@
         "schema-utils": "^3.0.0"
       },
       "dependencies": {
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "schema-utils": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
@@ -19930,6 +20172,20 @@
             "@types/json-schema": "^7.0.6",
             "ajv": "^6.12.5",
             "ajv-keywords": "^3.5.2"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         }
       }
@@ -20452,6 +20708,18 @@
           "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==",
           "dev": true
         },
+        "ajv": {
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
         "braces": {
           "version": "2.3.2",
           "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
@@ -20569,6 +20837,12 @@
           "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
           "dev": true
         },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "loader-utils": {
           "version": "1.4.0",
           "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
@@ -20973,6 +21247,12 @@
           "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
           "dev": true
         },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        },
         "kind-of": {
           "version": "3.2.2",
           "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@@ -21141,6 +21421,20 @@
             "ajv": "^6.1.0",
             "ajv-errors": "^1.0.0",
             "ajv-keywords": "^3.1.0"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "6.12.6",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+              "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+              "dev": true,
+              "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+              }
+            }
           }
         },
         "semver": {
@@ -21843,6 +22137,14 @@
         "property-expr": "^2.0.4",
         "toposort": "^2.0.2"
       }
+    },
+    "yup-faker": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yup-faker/-/yup-faker-0.1.0.tgz",
+      "integrity": "sha512-bQSVkIfW4Ny2Qay4i0rQU5eazsVgB1gi6nla6sHaMF92ouoTt+TSYwVbNbT7HqYslK3dg/VtypCAa5LUPVGliA==",
+      "requires": {
+        "faker": "^5.1.0"
+      }
     }
   }
 }

+ 4 - 1
kafka-ui-react-app/package.json

@@ -8,6 +8,7 @@
     "@hookform/error-message": "^2.0.0",
     "@hookform/resolvers": "^2.5.1",
     "@rooks/use-outside-click-ref": "^4.10.1",
+    "@types/yup": "^0.29.13",
     "@testing-library/react": "^12.0.0",
     "ace-builds": "^1.4.12",
     "bulma": "^0.9.3",
@@ -16,6 +17,7 @@
     "date-fns": "^2.19.0",
     "eslint-import-resolver-node": "^0.3.4",
     "eslint-import-resolver-typescript": "^2.4.0",
+    "json-schema-yup-transformer": "^1.6.0",
     "lodash": "^4.17.21",
     "node-fetch": "^2.6.1",
     "pretty-ms": "^7.0.1",
@@ -35,7 +37,8 @@
     "typesafe-actions": "^5.1.0",
     "use-debounce": "^7.0.0",
     "uuid": "^8.3.1",
-    "yup": "^0.32.9"
+    "yup": "^0.32.9",
+    "yup-faker": "^0.1.0"
   },
   "lint-staged": {
     "*.{js,ts,jsx,tsx}": [

+ 8 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -9,6 +9,7 @@ import {
   clusterTopicsPath,
   clusterTopicConsumerGroupsPath,
   clusterTopicEditPath,
+  clusterTopicSendMessagePath,
 } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
@@ -102,6 +103,13 @@ const Details: React.FC<Props> = ({
                   Delete Topic
                 </button>
 
+                <Link
+                  to={clusterTopicSendMessagePath(clusterName, topicName)}
+                  className="button"
+                >
+                  Produce message
+                </Link>
+
                 <Link
                   to={clusterTopicEditPath(clusterName, topicName)}
                   className="button"

+ 211 - 0
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx

@@ -0,0 +1,211 @@
+import JSONEditor from 'components/common/JSONEditor/JSONEditor';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+import {
+  CreateTopicMessage,
+  Partition,
+  TopicMessageSchema,
+} from 'generated-sources';
+import React from 'react';
+import { useForm, Controller } from 'react-hook-form';
+import convertToYup from 'json-schema-yup-transformer';
+import { getFakeData } from 'yup-faker';
+import { useHistory } from 'react-router';
+import { clusterTopicMessagesPath } from 'lib/paths';
+
+import validateMessage from './validateMessage';
+
+export interface Props {
+  clusterName: string;
+  topicName: string;
+  fetchTopicMessageSchema: (clusterName: string, topicName: string) => void;
+  sendTopicMessage: (
+    clusterName: string,
+    topicName: string,
+    payload: CreateTopicMessage
+  ) => void;
+  messageSchema: TopicMessageSchema | undefined;
+  schemaIsFetched: boolean;
+  messageIsSent: boolean;
+  messageIsSending: boolean;
+  partitions: Partition[];
+}
+
+const SendMessage: React.FC<Props> = ({
+  clusterName,
+  topicName,
+  fetchTopicMessageSchema,
+  sendTopicMessage,
+  messageSchema,
+  schemaIsFetched,
+  messageIsSent,
+  messageIsSending,
+  partitions,
+}) => {
+  const [keyExampleValue, setKeyExampleValue] = React.useState('');
+  const [contentExampleValue, setContentExampleValue] = React.useState('');
+  const [schemaErrorString, setSchemaErrorString] = React.useState('');
+  const {
+    register,
+    handleSubmit,
+    formState: { isSubmitting, isDirty },
+    control,
+  } = useForm({ mode: 'onChange' });
+  const history = useHistory();
+
+  React.useEffect(() => {
+    fetchTopicMessageSchema(clusterName, topicName);
+  }, []);
+  React.useEffect(() => {
+    if (schemaIsFetched && messageSchema) {
+      const validateKey = convertToYup(JSON.parse(messageSchema.key.schema));
+      if (validateKey) {
+        setKeyExampleValue(
+          JSON.stringify(getFakeData(validateKey), null, '\t')
+        );
+      }
+
+      const validateContent = convertToYup(
+        JSON.parse(messageSchema.value.schema)
+      );
+      if (validateContent) {
+        setContentExampleValue(
+          JSON.stringify(getFakeData(validateContent), null, '\t')
+        );
+      }
+    }
+  }, [schemaIsFetched]);
+  React.useEffect(() => {
+    if (messageIsSent) {
+      history.push(clusterTopicMessagesPath(clusterName, topicName));
+    }
+  }, [messageIsSent]);
+
+  const onSubmit = async (data: {
+    key: string;
+    content: string;
+    headers: string;
+    partition: number;
+  }) => {
+    if (messageSchema) {
+      const key = data.key || keyExampleValue;
+      const content = data.content || contentExampleValue;
+      const { partition } = data;
+      const headers = data.headers ? JSON.parse(data.headers) : undefined;
+      const messageIsValid = await validateMessage(
+        key,
+        content,
+        messageSchema,
+        setSchemaErrorString
+      );
+
+      if (messageIsValid) {
+        sendTopicMessage(clusterName, topicName, {
+          key,
+          content,
+          headers,
+          partition,
+        });
+      }
+    }
+  };
+
+  if (!keyExampleValue && !contentExampleValue) {
+    return <PageLoader />;
+  }
+  return (
+    <div className="box">
+      <form onSubmit={handleSubmit(onSubmit)}>
+        <div className="columns">
+          <div className="column is-one-third">
+            <label className="label" htmlFor="select">
+              Partition
+            </label>
+            <div className="select is-block">
+              <select
+                id="select"
+                defaultValue={partitions[0].partition}
+                disabled={isSubmitting || messageIsSending}
+                {...register('partition')}
+              >
+                {partitions.map((partition) => (
+                  <option key={partition.partition} value={partition.partition}>
+                    {partition.partition}
+                  </option>
+                ))}
+              </select>
+            </div>
+          </div>
+        </div>
+
+        <div className="columns">
+          <div className="column is-one-half">
+            <label className="label">Key</label>
+            <Controller
+              control={control}
+              name="key"
+              render={({ field: { name, onChange } }) => (
+                <JSONEditor
+                  readOnly={isSubmitting || messageIsSending}
+                  defaultValue={keyExampleValue}
+                  name={name}
+                  onChange={onChange}
+                />
+              )}
+            />
+          </div>
+          <div className="column is-one-half">
+            <label className="label">Content</label>
+            <Controller
+              control={control}
+              name="content"
+              render={({ field: { name, onChange } }) => (
+                <JSONEditor
+                  readOnly={isSubmitting || messageIsSending}
+                  defaultValue={contentExampleValue}
+                  name={name}
+                  onChange={onChange}
+                />
+              )}
+            />
+          </div>
+        </div>
+        <div className="columns">
+          <div className="column">
+            <label className="label">Headers</label>
+            <Controller
+              control={control}
+              name="headers"
+              render={({ field: { name, onChange } }) => (
+                <JSONEditor
+                  readOnly={isSubmitting || messageIsSending}
+                  defaultValue="{}"
+                  name={name}
+                  onChange={onChange}
+                  height="200px"
+                />
+              )}
+            />
+          </div>
+        </div>
+        {schemaErrorString && (
+          <div className="mb-4">
+            {schemaErrorString.split('-').map((err) => (
+              <p className="help is-danger" key={err}>
+                {err}
+              </p>
+            ))}
+          </div>
+        )}
+        <button
+          type="submit"
+          className="button is-primary"
+          disabled={!isDirty || isSubmitting || messageIsSending}
+        >
+          Send
+        </button>
+      </form>
+    </div>
+  );
+};
+
+export default SendMessage;

+ 46 - 0
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessageContainer.ts

@@ -0,0 +1,46 @@
+import { connect } from 'react-redux';
+import { RootState, ClusterName, TopicName } from 'redux/interfaces';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { fetchTopicMessageSchema, sendTopicMessage } from 'redux/actions';
+import {
+  getMessageSchemaByTopicName,
+  getPartitionsByTopicName,
+  getTopicMessageSchemaFetched,
+  getTopicMessageSending,
+  getTopicMessageSent,
+} from 'redux/reducers/topics/selectors';
+
+import SendMessage from './SendMessage';
+
+interface RouteProps {
+  clusterName: ClusterName;
+  topicName: TopicName;
+}
+
+type OwnProps = RouteComponentProps<RouteProps>;
+
+const mapStateToProps = (
+  state: RootState,
+  {
+    match: {
+      params: { topicName, clusterName },
+    },
+  }: OwnProps
+) => ({
+  clusterName,
+  topicName,
+  messageSchema: getMessageSchemaByTopicName(state, topicName),
+  schemaIsFetched: getTopicMessageSchemaFetched(state),
+  messageIsSent: getTopicMessageSent(state),
+  messageIsSending: getTopicMessageSending(state),
+  partitions: getPartitionsByTopicName(state, topicName),
+});
+
+const mapDispatchToProps = {
+  fetchTopicMessageSchema,
+  sendTopicMessage,
+};
+
+export default withRouter(
+  connect(mapStateToProps, mapDispatchToProps)(SendMessage)
+);

+ 139 - 0
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx

@@ -0,0 +1,139 @@
+import React from 'react';
+import SendMessage, {
+  Props,
+} from 'components/Topics/Topic/SendMessage/SendMessage';
+import { MessageSchemaSourceEnum } from 'generated-sources';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+
+const mockConvertToYup = jest
+  .fn()
+  .mockReturnValue(() => ({ validate: () => true }));
+
+jest.mock('yup-faker', () => ({
+  getFakeData: () => ({
+    f1: -93251214,
+    schema: 'enim sit in fugiat dolor',
+    f2: 'deserunt culpa sunt',
+  }),
+}));
+
+const setupWrapper = (props?: Partial<Props>) => (
+  <SendMessage
+    clusterName="testCluster"
+    topicName="testTopic"
+    fetchTopicMessageSchema={jest.fn()}
+    sendTopicMessage={jest.fn()}
+    messageSchema={{
+      key: {
+        name: 'key',
+        source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+        schema: `{
+          "$schema": "http://json-schema.org/draft-07/schema#",
+          "$id": "http://example.com/myURI.schema.json",
+          "title": "TestRecord",
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "f1": {
+              "type": "integer"
+            },
+            "f2": {
+              "type": "string"
+            },
+            "schema": {
+              "type": "string"
+            }
+          }
+        }
+        `,
+      },
+      value: {
+        name: 'value',
+        source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+        schema: `{
+          "$schema": "http://json-schema.org/draft-07/schema#",
+          "$id": "http://example.com/myURI1.schema.json",
+          "title": "TestRecord",
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "f1": {
+              "type": "integer"
+            },
+            "f2": {
+              "type": "string"
+            },
+            "schema": {
+              "type": "string"
+            }
+          }
+        }
+        `,
+      },
+    }}
+    schemaIsFetched={false}
+    messageIsSent={false}
+    messageIsSending={false}
+    partitions={[
+      {
+        partition: 0,
+        leader: 2,
+        replicas: [
+          {
+            broker: 2,
+            leader: false,
+            inSync: true,
+          },
+        ],
+        offsetMax: 0,
+        offsetMin: 0,
+      },
+      {
+        partition: 1,
+        leader: 1,
+        replicas: [
+          {
+            broker: 1,
+            leader: false,
+            inSync: true,
+          },
+        ],
+        offsetMax: 0,
+        offsetMin: 0,
+      },
+    ]}
+    {...props}
+  />
+);
+
+describe('SendMessage', () => {
+  it('calls fetchTopicMessageSchema on first render', () => {
+    const fetchTopicMessageSchemaMock = jest.fn();
+    render(
+      setupWrapper({ fetchTopicMessageSchema: fetchTopicMessageSchemaMock })
+    );
+    expect(fetchTopicMessageSchemaMock).toHaveBeenCalledTimes(1);
+  });
+
+  describe('when schema is fetched', () => {
+    it('calls sendTopicMessage on submit', async () => {
+      jest.mock('json-schema-yup-transformer', () => mockConvertToYup);
+      jest.mock('../validateMessage', () => jest.fn().mockReturnValue(true));
+      const mockSendTopicMessage = jest.fn();
+      render(
+        setupWrapper({
+          schemaIsFetched: true,
+          sendTopicMessage: mockSendTopicMessage,
+        })
+      );
+      const select = await screen.findByLabelText('Partition');
+      fireEvent.change(select, {
+        target: { value: 2 },
+      });
+      await waitFor(async () => {
+        fireEvent.click(await screen.findByText('Send'));
+        expect(mockSendTopicMessage).toHaveBeenCalledTimes(1);
+      });
+    });
+  });
+});

+ 50 - 0
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/fixtures.ts

@@ -0,0 +1,50 @@
+import { MessageSchemaSourceEnum } from 'generated-sources';
+
+export const testSchema = {
+  key: {
+    name: 'key',
+    source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+    schema: `{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "http://example.com/myURI.schema.json",
+  "title": "TestRecord",
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "f1": {
+      "type": "integer"
+    },
+    "f2": {
+      "type": "string"
+    },
+    "schema": {
+      "type": "string"
+    }
+  }
+}
+`,
+  },
+  value: {
+    name: 'value',
+    source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+    schema: `{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "http://example.com/myURI1.schema.json",
+  "title": "TestRecord",
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "f1": {
+      "type": "integer"
+    },
+    "f2": {
+      "type": "string"
+    },
+    "schema": {
+      "type": "string"
+    }
+  }
+}
+`,
+  },
+};

+ 47 - 0
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/validateMessage.spec.ts

@@ -0,0 +1,47 @@
+import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage';
+
+import { testSchema } from './fixtures';
+
+describe('validateMessage', () => {
+  it('returns true on correct input data', async () => {
+    const mockSetError = jest.fn();
+    expect(
+      await validateMessage(
+        `{
+      "f1": 32,
+      "f2": "multi-state",
+      "schema": "Bedfordshire violet SAS"
+    }`,
+        `{
+      "f1": 21128,
+      "f2": "Health Berkshire Re-engineered",
+      "schema": "Dynamic Greenland Beauty"
+    }`,
+        testSchema,
+        mockSetError
+      )
+    ).toBe(true);
+    expect(mockSetError).toHaveBeenCalledTimes(1);
+  });
+
+  it('returns false on incorrect input data', async () => {
+    const mockSetError = jest.fn();
+    expect(
+      await validateMessage(
+        `{
+      "f1": "32",
+      "f2": "multi-state",
+      "schema": "Bedfordshire violet SAS"
+    }`,
+        `{
+      "f1": "21128",
+      "f2": "Health Berkshire Re-engineered",
+      "schema": "Dynamic Greenland Beauty"
+    }`,
+        testSchema,
+        mockSetError
+      )
+    ).toBe(false);
+    expect(mockSetError).toHaveBeenCalledTimes(3);
+  });
+});

+ 67 - 0
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/validateMessage.ts

@@ -0,0 +1,67 @@
+import { TopicMessageSchema } from 'generated-sources';
+import convertToYup from 'json-schema-yup-transformer';
+
+const validateMessage = async (
+  key: string,
+  content: string,
+  messageSchema: TopicMessageSchema | undefined,
+  setSchemaErrorString: React.Dispatch<React.SetStateAction<string>>
+): Promise<boolean> => {
+  setSchemaErrorString('');
+  try {
+    if (messageSchema) {
+      const validateKey = convertToYup(JSON.parse(messageSchema.key.schema));
+      const validateContent = convertToYup(
+        JSON.parse(messageSchema.value.schema)
+      );
+      let keyIsValid = false;
+      let contentIsValid = false;
+
+      try {
+        await validateKey?.validate(JSON.parse(key));
+        keyIsValid = true;
+      } catch (err) {
+        let errorString = '';
+        if (err.errors) {
+          err.errors.forEach((e: string) => {
+            errorString = errorString ? `${errorString}-Key ${e}` : `Key ${e}`;
+          });
+        } else {
+          errorString = errorString
+            ? `${errorString}-Key ${err.message}`
+            : `Key ${err.message}`;
+        }
+
+        setSchemaErrorString((e) => (e ? `${e}-${errorString}` : errorString));
+      }
+      try {
+        await validateContent?.validate(JSON.parse(content));
+        contentIsValid = true;
+      } catch (err) {
+        let errorString = '';
+        if (err.errors) {
+          err.errors.forEach((e: string) => {
+            errorString = errorString
+              ? `${errorString}-Content ${e}`
+              : `Content ${e}`;
+          });
+        } else {
+          errorString = errorString
+            ? `${errorString}-Content ${err.message}`
+            : `Content ${err.message}`;
+        }
+
+        setSchemaErrorString((e) => (e ? `${e}-${errorString}` : errorString));
+      }
+
+      if (keyIsValid && contentIsValid) {
+        return true;
+      }
+    }
+  } catch (err) {
+    setSchemaErrorString((e) => (e ? `${e}-${err.message}` : err.message));
+  }
+  return false;
+};
+
+export default validateMessage;

+ 12 - 0
kafka-ui-react-app/src/components/Topics/Topic/Topic.tsx

@@ -7,6 +7,8 @@ import EditContainer from 'components/Topics/Topic/Edit/EditContainer';
 import DetailsContainer from 'components/Topics/Topic/Details/DetailsContainer';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 
+import SendMessageContainer from './SendMessage/SendMessageContainer';
+
 interface RouterParams {
   clusterName: ClusterName;
   topicName: TopicName;
@@ -49,6 +51,11 @@ const Topic: React.FC<TopicProps> = ({
       <div className="level">
         <div className="level-item level-left">
           <Switch>
+            <Route exact path={`${topicPageUrl}/message`}>
+              <Breadcrumb links={childBreadcrumbLinks}>
+                Produce Message
+              </Breadcrumb>
+            </Route>
             <Route exact path={`${topicPageUrl}/edit`}>
               <Breadcrumb links={childBreadcrumbLinks}>Edit</Breadcrumb>
             </Route>
@@ -67,6 +74,11 @@ const Topic: React.FC<TopicProps> = ({
             path="/ui/clusters/:clusterName/topics/:topicName/edit"
             component={EditContainer}
           />
+          <Route
+            exact
+            path="/ui/clusters/:clusterName/topics/:topicName/message"
+            component={SendMessageContainer}
+          />
           <Route
             path="/ui/clusters/:clusterName/topics/:topicName"
             component={DetailsContainer}

+ 4 - 0
kafka-ui-react-app/src/lib/paths.ts

@@ -60,6 +60,10 @@ export const clusterTopicConsumerGroupsPath = (
   clusterName: ClusterName,
   topicName: TopicName
 ) => `${clusterTopicsPath(clusterName)}/${topicName}/consumergroups`;
+export const clusterTopicSendMessagePath = (
+  clusterName: ClusterName,
+  topicName: TopicName
+) => `${clusterTopicsPath(clusterName)}/${topicName}/message`;
 
 // Kafka Connect
 export const clusterConnectsPath = (clusterName: ClusterName) =>

+ 90 - 1
kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts

@@ -3,7 +3,11 @@ import {
   schemaVersionsPayload,
 } from 'redux/reducers/schemas/__test__/fixtures';
 import * as actions from 'redux/actions';
-import { TopicColumnsToSort } from 'generated-sources';
+import {
+  MessageSchemaSourceEnum,
+  TopicColumnsToSort,
+  TopicMessageSchema,
+} from 'generated-sources';
 import { FailurePayload } from 'redux/interfaces';
 
 import { mockTopicsState } from './fixtures';
@@ -202,4 +206,89 @@ describe('Actions', () => {
       });
     });
   });
+
+  describe('sending messages', () => {
+    describe('fetchTopicMessageSchemaAction', () => {
+      it('creates GET_TOPIC_SCHEMA__REQUEST', () => {
+        expect(actions.fetchTopicMessageSchemaAction.request()).toEqual({
+          type: 'GET_TOPIC_SCHEMA__REQUEST',
+        });
+      });
+      it('creates GET_TOPIC_SCHEMA__SUCCESS', () => {
+        const messageSchema: TopicMessageSchema = {
+          key: {
+            name: 'key',
+            source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+            schema: `{
+        "$schema": "http://json-schema.org/draft-07/schema#",
+        "$id": "http://example.com/myURI.schema.json",
+        "title": "TestRecord",
+        "type": "object",
+        "additionalProperties": false,
+        "properties": {
+          "f1": {
+            "type": "integer"
+          },
+          "f2": {
+            "type": "string"
+          },
+          "schema": {
+            "type": "string"
+          }
+        }
+        }
+        `,
+          },
+          value: {
+            name: 'value',
+            source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+            schema: `{
+        "$schema": "http://json-schema.org/draft-07/schema#",
+        "$id": "http://example.com/myURI1.schema.json",
+        "title": "TestRecord",
+        "type": "object",
+        "additionalProperties": false,
+        "properties": {
+          "f1": {
+            "type": "integer"
+          },
+          "f2": {
+            "type": "string"
+          },
+          "schema": {
+            "type": "string"
+          }
+        }
+        }
+        `,
+          },
+        };
+        expect(
+          actions.fetchTopicMessageSchemaAction.success({
+            topicName: 'test',
+            schema: messageSchema,
+          })
+        ).toEqual({
+          type: 'GET_TOPIC_SCHEMA__SUCCESS',
+          payload: {
+            topicName: 'test',
+            schema: messageSchema,
+          },
+        });
+      });
+
+      it('creates GET_TOPIC_SCHEMA__FAILURE', () => {
+        const alert: FailurePayload = {
+          subject: ['message-chema', 'test'].join('-'),
+          title: `Message Schema Test`,
+        };
+        expect(
+          actions.fetchTopicMessageSchemaAction.failure({ alert })
+        ).toEqual({
+          type: 'GET_TOPIC_SCHEMA__FAILURE',
+          payload: { alert },
+        });
+      });
+    });
+  });
 });

+ 141 - 0
kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts

@@ -3,6 +3,7 @@ import * as actions from 'redux/actions/actions';
 import * as thunks from 'redux/actions/thunks';
 import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
 import { mockTopicsState } from 'redux/actions/__test__/fixtures';
+import { MessageSchemaSourceEnum, TopicMessageSchema } from 'generated-sources';
 import { FailurePayload } from 'redux/interfaces';
 import { getResponse } from 'lib/errorHandling';
 
@@ -135,6 +136,146 @@ describe('Thunks', () => {
     });
   });
 
+  describe('fetchTopicMessageSchema', () => {
+    it('creates GET_TOPIC_SCHEMA__FAILURE', async () => {
+      fetchMock.getOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/messages/schema`,
+        404
+      );
+      try {
+        await store.dispatch(
+          thunks.fetchTopicMessageSchema(clusterName, topicName)
+        );
+      } catch (error) {
+        expect(error.status).toEqual(404);
+        expect(store.getActions()).toEqual([
+          actions.fetchTopicMessageSchemaAction.request(),
+          actions.fetchTopicMessageSchemaAction.failure({
+            alert: {
+              subject: ['topic', topicName].join('-'),
+              title: `Topic Schema ${topicName}`,
+              response: error,
+            },
+          }),
+        ]);
+      }
+    });
+
+    it('creates GET_TOPIC_SCHEMA__SUCCESS', async () => {
+      const messageSchema: TopicMessageSchema = {
+        key: {
+          name: 'key',
+          source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+          schema: `{
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "$id": "http://example.com/myURI.schema.json",
+      "title": "TestRecord",
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "f1": {
+          "type": "integer"
+        },
+        "f2": {
+          "type": "string"
+        },
+        "schema": {
+          "type": "string"
+        }
+      }
+      }
+      `,
+        },
+        value: {
+          name: 'value',
+          source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+          schema: `{
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "$id": "http://example.com/myURI1.schema.json",
+      "title": "TestRecord",
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "f1": {
+          "type": "integer"
+        },
+        "f2": {
+          "type": "string"
+        },
+        "schema": {
+          "type": "string"
+        }
+      }
+      }
+      `,
+        },
+      };
+      fetchMock.getOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/messages/schema`,
+        messageSchema
+      );
+      await store.dispatch(
+        thunks.fetchTopicMessageSchema(clusterName, topicName)
+      );
+      expect(store.getActions()).toEqual([
+        actions.fetchTopicMessageSchemaAction.request(),
+        actions.fetchTopicMessageSchemaAction.success({
+          topicName,
+          schema: messageSchema,
+        }),
+      ]);
+    });
+  });
+
+  describe('sendTopicMessage', () => {
+    it('creates SEND_TOPIC_MESSAGE__FAILURE', async () => {
+      fetchMock.postOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/messages`,
+        404
+      );
+      try {
+        await store.dispatch(
+          thunks.sendTopicMessage(clusterName, topicName, {
+            key: '{}',
+            content: '{}',
+            headers: undefined,
+            partition: 0,
+          })
+        );
+      } catch (error) {
+        expect(error.status).toEqual(404);
+        expect(store.getActions()).toEqual([
+          actions.sendTopicMessageAction.request(),
+          actions.sendTopicMessageAction.failure({
+            alert: {
+              subject: ['topic', topicName].join('-'),
+              title: `Topic Message ${topicName}`,
+              response: error,
+            },
+          }),
+        ]);
+      }
+    });
+
+    it('creates SEND_TOPIC_MESSAGE__SUCCESS', async () => {
+      fetchMock.postOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/messages`,
+        200
+      );
+      await store.dispatch(
+        thunks.sendTopicMessage(clusterName, topicName, {
+          key: '{}',
+          content: '{}',
+          headers: undefined,
+          partition: 0,
+        })
+      );
+      expect(store.getActions()).toEqual([
+        actions.sendTopicMessageAction.request(),
+        actions.sendTopicMessageAction.success(),
+      ]);
+    });
+  });
   describe('increasing partitions count', () => {
     it('calls updateTopicPartitionsCountAction.success on success', async () => {
       fetchMock.patchOnce(

+ 17 - 0
kafka-ui-react-app/src/redux/actions/actions.ts

@@ -23,6 +23,7 @@ import {
   FullConnectorInfo,
   Connect,
   Task,
+  TopicMessageSchema,
 } from 'generated-sources';
 
 export const fetchClusterStatsAction = createAsyncAction(
@@ -254,6 +255,22 @@ export const fetchTopicConsumerGroupsAction = createAsyncAction(
   'GET_TOPIC_CONSUMER_GROUPS__FAILURE'
 )<undefined, TopicsState, undefined>();
 
+export const fetchTopicMessageSchemaAction = createAsyncAction(
+  'GET_TOPIC_SCHEMA__REQUEST',
+  'GET_TOPIC_SCHEMA__SUCCESS',
+  'GET_TOPIC_SCHEMA__FAILURE'
+)<
+  undefined,
+  { topicName: string; schema: TopicMessageSchema },
+  { alert?: FailurePayload }
+>();
+
+export const sendTopicMessageAction = createAsyncAction(
+  'SEND_TOPIC_MESSAGE__REQUEST',
+  'SEND_TOPIC_MESSAGE__SUCCESS',
+  'SEND_TOPIC_MESSAGE__FAILURE'
+)<undefined, undefined, { alert?: FailurePayload }>();
+
 export const updateTopicPartitionsCountAction = createAsyncAction(
   'UPDATE_PARTITIONS__REQUEST',
   'UPDATE_PARTITIONS__SUCCESS',

+ 54 - 0
kafka-ui-react-app/src/redux/actions/thunks/topics.ts

@@ -9,6 +9,7 @@ import {
   TopicConfig,
   TopicColumnsToSort,
   ConsumerGroupsApi,
+  CreateTopicMessage,
 } from 'generated-sources';
 import {
   PromiseThunkResult,
@@ -342,6 +343,59 @@ export const fetchTopicConsumerGroups =
     }
   };
 
+export const fetchTopicMessageSchema =
+  (clusterName: ClusterName, topicName: TopicName): PromiseThunkResult =>
+  async (dispatch) => {
+    dispatch(actions.fetchTopicMessageSchemaAction.request());
+    try {
+      const schema = await messagesApiClient.getTopicSchema({
+        clusterName,
+        topicName,
+      });
+      dispatch(
+        actions.fetchTopicMessageSchemaAction.success({ topicName, schema })
+      );
+    } catch (e) {
+      const response = await getResponse(e);
+      const alert: FailurePayload = {
+        subject: ['topic', topicName].join('-'),
+        title: `Topic Schema ${topicName}`,
+        response,
+      };
+      dispatch(actions.fetchTopicMessageSchemaAction.failure({ alert }));
+    }
+  };
+
+export const sendTopicMessage =
+  (
+    clusterName: ClusterName,
+    topicName: TopicName,
+    payload: CreateTopicMessage
+  ): PromiseThunkResult =>
+  async (dispatch) => {
+    dispatch(actions.sendTopicMessageAction.request());
+    try {
+      await messagesApiClient.sendTopicMessages({
+        clusterName,
+        topicName,
+        createTopicMessage: {
+          key: payload.key,
+          content: payload.content,
+          headers: payload.headers,
+          partition: payload.partition,
+        },
+      });
+      dispatch(actions.sendTopicMessageAction.success());
+    } catch (e) {
+      const response = await getResponse(e);
+      const alert: FailurePayload = {
+        subject: ['topic', topicName].join('-'),
+        title: `Topic Message ${topicName}`,
+        response,
+      };
+      dispatch(actions.sendTopicMessageAction.failure({ alert }));
+    }
+  };
 export const updateTopicPartitionsCount =
   (
     clusterName: ClusterName,

+ 2 - 0
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -7,6 +7,7 @@ import {
   GetTopicMessagesRequest,
   ConsumerGroup,
   TopicColumnsToSort,
+  TopicMessageSchema,
 } from 'generated-sources';
 
 export type TopicName = Topic['name'];
@@ -42,6 +43,7 @@ export interface TopicFormCustomParams {
 export interface TopicWithDetailedInfo extends Topic, TopicDetails {
   config?: TopicConfig[];
   consumerGroups?: ConsumerGroup[];
+  messageSchema?: TopicMessageSchema;
 }
 
 export interface TopicsState {

+ 79 - 2
kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts

@@ -1,10 +1,11 @@
-import { TopicColumnsToSort } from 'generated-sources';
+import { MessageSchemaSourceEnum, TopicColumnsToSort } from 'generated-sources';
 import {
   deleteTopicAction,
   clearMessagesTopicAction,
   setTopicsSearchAction,
   setTopicsOrderByAction,
   fetchTopicConsumerGroupsAction,
+  fetchTopicMessageSchemaAction,
 } from 'redux/actions';
 import reducer from 'redux/reducers/topics/reducer';
 
@@ -13,7 +14,56 @@ const topic = {
   id: 'id',
 };
 
-const state = {
+const messageSchema = {
+  key: {
+    name: 'key',
+    source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+    schema: `{
+"$schema": "http://json-schema.org/draft-07/schema#",
+"$id": "http://example.com/myURI.schema.json",
+"title": "TestRecord",
+"type": "object",
+"additionalProperties": false,
+"properties": {
+  "f1": {
+    "type": "integer"
+  },
+  "f2": {
+    "type": "string"
+  },
+  "schema": {
+    "type": "string"
+  }
+}
+}
+`,
+  },
+  value: {
+    name: 'value',
+    source: MessageSchemaSourceEnum.SCHEMA_REGISTRY,
+    schema: `{
+"$schema": "http://json-schema.org/draft-07/schema#",
+"$id": "http://example.com/myURI1.schema.json",
+"title": "TestRecord",
+"type": "object",
+"additionalProperties": false,
+"properties": {
+  "f1": {
+    "type": "integer"
+  },
+  "f2": {
+    "type": "string"
+  },
+  "schema": {
+    "type": "string"
+  }
+}
+}
+`,
+  },
+};
+
+let state = {
   byName: {
     [topic.name]: topic,
   },
@@ -70,4 +120,31 @@ describe('topics reducer', () => {
       ).toEqual(state);
     });
   });
+
+  describe('message sending', () => {
+    it('adds message shema after fetching it', () => {
+      state = {
+        byName: {
+          [topic.name]: topic,
+        },
+        allNames: [topic.name],
+        messages: [],
+        totalPages: 1,
+        search: '',
+        orderBy: null,
+        consumerGroups: [],
+      };
+      expect(
+        reducer(
+          state,
+          fetchTopicMessageSchemaAction.success({
+            topicName: 'topic',
+            schema: messageSchema,
+          })
+        ).byName
+      ).toEqual({
+        [topic.name]: { ...topic, messageSchema },
+      });
+    });
+  });
 });

+ 11 - 1
kafka-ui-react-app/src/redux/reducers/topics/reducer.ts

@@ -2,6 +2,7 @@ import { TopicMessage } from 'generated-sources';
 import { Action, TopicsState } from 'redux/interfaces';
 import { getType } from 'typesafe-actions';
 import * as actions from 'redux/actions';
+import * as _ from 'lodash';
 
 export const initialState: TopicsState = {
   byName: {},
@@ -26,7 +27,7 @@ const transformTopicMessages = (
       try {
         parsedContent =
           typeof content !== 'object' ? JSON.parse(content) : content;
-      } catch (_) {
+      } catch (err) {
         // do nothing
       }
     }
@@ -75,6 +76,15 @@ const reducer = (state = initialState, action: Action): TopicsState => {
         orderBy: action.payload,
       };
     }
+    case getType(actions.fetchTopicMessageSchemaAction.success): {
+      const { topicName, schema } = action.payload;
+      const newState = _.cloneDeep(state);
+      newState.byName[topicName] = {
+        ...newState.byName[topicName],
+        messageSchema: schema,
+      };
+      return newState;
+    }
     default:
       return state;
   }

+ 24 - 0
kafka-ui-react-app/src/redux/reducers/topics/selectors.ts

@@ -25,6 +25,10 @@ const getTopicMessagesFetchingStatus =
 const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
 const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
 const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
+const getTopicMessageSchemaFetchingStatus =
+  createFetchingSelector('GET_TOPIC_SCHEMA');
+const getTopicMessageSendingStatus =
+  createFetchingSelector('SEND_TOPIC_MESSAGE');
 const getPartitionsCountIncreaseStatus =
   createFetchingSelector('UPDATE_PARTITIONS');
 const getReplicationFactorUpdateStatus = createFetchingSelector(
@@ -71,6 +75,21 @@ export const getTopicUpdated = createSelector(
   (status) => status === 'fetched'
 );
 
+export const getTopicMessageSchemaFetched = createSelector(
+  getTopicMessageSchemaFetchingStatus,
+  (status) => status === 'fetched'
+);
+
+export const getTopicMessageSent = createSelector(
+  getTopicMessageSendingStatus,
+  (status) => status === 'fetched'
+);
+
+export const getTopicMessageSending = createSelector(
+  getTopicMessageSendingStatus,
+  (status) => status === 'fetching'
+);
+
 export const getTopicPartitionsCountIncreased = createSelector(
   getPartitionsCountIncreaseStatus,
   (status) => status === 'fetched'
@@ -157,3 +176,8 @@ export const getTopicConsumerGroups = createSelector(
   getTopicName,
   (topics, topicName) => topics[topicName].consumerGroups || []
 );
+
+export const getMessageSchemaByTopicName = createSelector(
+  getTopicByName,
+  (topic) => topic.messageSchema
+);