Sending messages (#675)

* Implement message sending
This commit is contained in:
Alexander Krivonosov 2021-07-29 10:26:40 +03:00 committed by GitHub
parent 268c871312
commit ad8598a1d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1342 additions and 39 deletions

View file

@ -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"
}
}
}
}

View file

@ -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}": [

View file

@ -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"

View file

@ -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;

View file

@ -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)
);

View file

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

View file

@ -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"
}
}
}
`,
},
};

View file

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

View file

@ -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;

View file

@ -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}

View file

@ -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) =>

View file

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

View file

@ -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(

View file

@ -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',

View file

@ -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,

View file

@ -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 {

View file

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

View file

@ -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;
}

View file

@ -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
);