Forráskód Böngészése

Allow increasing the number of partitions and updating of the replication factor (#544)

Alexander Krivonosov 3 éve
szülő
commit
8da350ea38

+ 71 - 40
kafka-ui-react-app/package-lock.json

@@ -8,7 +8,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
       "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
-      "dev": true,
       "requires": {
         "@babel/highlight": "^7.14.5"
       }
@@ -341,8 +340,7 @@
     "@babel/helper-validator-identifier": {
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz",
-      "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==",
-      "dev": true
+      "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg=="
     },
     "@babel/helper-validator-option": {
       "version": "7.14.5",
@@ -377,7 +375,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
       "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
-      "dev": true,
       "requires": {
         "@babel/helper-validator-identifier": "^7.14.5",
         "chalk": "^2.0.0",
@@ -388,7 +385,6 @@
           "version": "3.2.1",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
           "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
           "requires": {
             "color-convert": "^1.9.0"
           }
@@ -397,7 +393,6 @@
           "version": "2.4.2",
           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
           "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "dev": true,
           "requires": {
             "ansi-styles": "^3.2.1",
             "escape-string-regexp": "^1.0.5",
@@ -407,14 +402,12 @@
         "has-flag": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
-          "dev": true
+          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
         },
         "supports-color": {
           "version": "5.5.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
           "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
           }
@@ -1346,7 +1339,6 @@
       "version": "7.14.6",
       "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.6.tgz",
       "integrity": "sha512-Xl8SPYtdjcMoCsIM4teyVRg7jIcgl8F2kRtoCcXuHzXswt9UxZCS6BzRo8fcnCuP6u2XtPgvyonmEPF57Kxo9Q==",
-      "dev": true,
       "requires": {
         "core-js-pure": "^3.14.0",
         "regenerator-runtime": "^0.13.4"
@@ -2118,7 +2110,6 @@
       "version": "27.0.6",
       "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.0.6.tgz",
       "integrity": "sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g==",
-      "dev": true,
       "requires": {
         "@types/istanbul-lib-coverage": "^2.0.0",
         "@types/istanbul-reports": "^3.0.0",
@@ -2131,7 +2122,6 @@
           "version": "16.0.4",
           "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
           "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
-          "dev": true,
           "requires": {
             "@types/yargs-parser": "*"
           }
@@ -2514,6 +2504,44 @@
         "loader-utils": "^2.0.0"
       }
     },
+    "@testing-library/dom": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.1.0.tgz",
+      "integrity": "sha512-kmW9alndr19qd6DABzQ978zKQ+J65gU2Rzkl8hriIetPnwpesRaK4//jEQyYh8fEALmGhomD/LBQqt+o+DL95Q==",
+      "requires": {
+        "@babel/code-frame": "^7.10.4",
+        "@babel/runtime": "^7.12.5",
+        "@types/aria-query": "^4.2.0",
+        "aria-query": "^4.2.2",
+        "chalk": "^4.1.0",
+        "dom-accessibility-api": "^0.5.6",
+        "lz-string": "^1.4.4",
+        "pretty-format": "^27.0.2"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+          "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
+        },
+        "pretty-format": {
+          "version": "27.0.6",
+          "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz",
+          "integrity": "sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ==",
+          "requires": {
+            "@jest/types": "^27.0.6",
+            "ansi-regex": "^5.0.0",
+            "ansi-styles": "^5.0.0",
+            "react-is": "^17.0.1"
+          }
+        },
+        "react-is": {
+          "version": "17.0.2",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+          "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+        }
+      }
+    },
     "@testing-library/jest-dom": {
       "version": "5.14.1",
       "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz",
@@ -2543,6 +2571,15 @@
         }
       }
     },
+    "@testing-library/react": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.0.0.tgz",
+      "integrity": "sha512-sh3jhFgEshFyJ/0IxGltRhwZv2kFKfJ3fN1vTZ6hhMXzz9ZbbcTgmDYM4e+zJv+oiVKKEWZPyqPAh4MQBI65gA==",
+      "requires": {
+        "@babel/runtime": "^7.12.5",
+        "@testing-library/dom": "^8.0.0"
+      }
+    },
     "@tootallnate/once": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -2573,6 +2610,11 @@
       "integrity": "sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA==",
       "dev": true
     },
+    "@types/aria-query": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
+      "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig=="
+    },
     "@types/babel__core": {
       "version": "7.1.14",
       "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz",
@@ -2726,14 +2768,12 @@
     "@types/istanbul-lib-coverage": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
-      "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==",
-      "dev": true
+      "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw=="
     },
     "@types/istanbul-lib-report": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
       "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
-      "dev": true,
       "requires": {
         "@types/istanbul-lib-coverage": "*"
       }
@@ -2742,7 +2782,6 @@
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
       "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
-      "dev": true,
       "requires": {
         "@types/istanbul-lib-report": "*"
       }
@@ -2790,8 +2829,7 @@
     "@types/node": {
       "version": "16.0.0",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.0.0.tgz",
-      "integrity": "sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg==",
-      "dev": true
+      "integrity": "sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg=="
     },
     "@types/node-fetch": {
       "version": "2.5.10",
@@ -2957,9 +2995,9 @@
       "dev": true
     },
     "@types/testing-library__jest-dom": {
-      "version": "5.14.0",
-      "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.0.tgz",
-      "integrity": "sha512-l2P2GO+hFF4Liye+fAajT1qBqvZOiL79YMpEvgGs1xTK7hECxBI8Wz4J7ntACJNiJ9r0vXQqYovroXRLPDja6A==",
+      "version": "5.14.1",
+      "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz",
+      "integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==",
       "dev": true,
       "requires": {
         "@types/jest": "*"
@@ -3052,8 +3090,7 @@
     "@types/yargs-parser": {
       "version": "20.2.0",
       "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz",
-      "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==",
-      "dev": true
+      "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
     },
     "@typescript-eslint/eslint-plugin": {
       "version": "4.28.1",
@@ -3720,14 +3757,12 @@
     "ansi-regex": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
-      "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
-      "dev": true
+      "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
     },
     "ansi-styles": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
       "requires": {
         "color-convert": "^2.0.1"
       },
@@ -3736,7 +3771,6 @@
           "version": "2.0.1",
           "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
           "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
           "requires": {
             "color-name": "~1.1.4"
           }
@@ -3744,8 +3778,7 @@
         "color-name": {
           "version": "1.1.4",
           "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-          "dev": true
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
         }
       }
     },
@@ -3937,7 +3970,6 @@
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
       "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
-      "dev": true,
       "requires": {
         "@babel/runtime": "^7.10.2",
         "@babel/runtime-corejs3": "^7.10.2"
@@ -5293,7 +5325,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
       "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
-      "dev": true,
       "requires": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
@@ -5968,8 +5999,7 @@
     "core-js-pure": {
       "version": "3.14.0",
       "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.14.0.tgz",
-      "integrity": "sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g==",
-      "dev": true
+      "integrity": "sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g=="
     },
     "core-util-is": {
       "version": "1.0.2",
@@ -6842,8 +6872,7 @@
     "dom-accessibility-api": {
       "version": "0.5.6",
       "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz",
-      "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==",
-      "dev": true
+      "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw=="
     },
     "dom-converter": {
       "version": "0.2.0",
@@ -7379,8 +7408,7 @@
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
-      "dev": true
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
     },
     "escodegen": {
       "version": "2.0.0",
@@ -9484,8 +9512,7 @@
     "has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
     },
     "has-symbols": {
       "version": "1.0.2",
@@ -12924,6 +12951,11 @@
         "yallist": "^4.0.0"
       }
     },
+    "lz-string": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
+      "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
+    },
     "magic-string": {
       "version": "0.25.7",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@@ -18856,7 +18888,6 @@
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
       "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
       "requires": {
         "has-flag": "^4.0.0"
       }

+ 2 - 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",
+    "@testing-library/react": "^12.0.0",
     "ace-builds": "^1.4.12",
     "bulma": "^0.9.3",
     "bulma-switch": "^2.0.0",
@@ -73,7 +74,7 @@
   "devDependencies": {
     "@jest/types": "^27.0.6",
     "@openapitools/openapi-generator-cli": "^2.3.5",
-    "@testing-library/jest-dom": "^5.11.10",
+    "@testing-library/jest-dom": "^5.14.1",
     "@types/classnames": "^2.2.11",
     "@types/enzyme": "^3.10.8",
     "@types/jest": "^26.0.21",

+ 200 - 0
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone.tsx

@@ -0,0 +1,200 @@
+import { ErrorMessage } from '@hookform/error-message';
+import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
+import React from 'react';
+import { useForm } from 'react-hook-form';
+
+export interface Props {
+  clusterName: string;
+  topicName: string;
+  defaultPartitions: number;
+  defaultReplicationFactor: number;
+  partitionsCountIncreased: boolean;
+  replicationFactorUpdated: boolean;
+  updateTopicPartitionsCount: (
+    clusterName: string,
+    topicname: string,
+    partitions: number
+  ) => void;
+  updateTopicReplicationFactor: (
+    clusterName: string,
+    topicname: string,
+    replicationFactor: number
+  ) => void;
+}
+
+const DangerZone: React.FC<Props> = ({
+  clusterName,
+  topicName,
+  defaultPartitions,
+  defaultReplicationFactor,
+  partitionsCountIncreased,
+  replicationFactorUpdated,
+  updateTopicPartitionsCount,
+  updateTopicReplicationFactor,
+}) => {
+  const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] =
+    React.useState<boolean>(false);
+  const [
+    isReplicationFactorConfirmationVisible,
+    setIsReplicationFactorConfirmationVisible,
+  ] = React.useState<boolean>(false);
+  const [partitions, setPartitions] = React.useState<number>(defaultPartitions);
+  const [replicationFactor, setReplicationFactor] = React.useState<number>(
+    defaultReplicationFactor
+  );
+
+  const {
+    register: partitionsRegister,
+    handleSubmit: handlePartitionsSubmit,
+    formState: partitionsFormState,
+    setError: setPartitionsError,
+    getValues: partitionsGetValues,
+  } = useForm({
+    defaultValues: {
+      partitions,
+    },
+  });
+
+  const {
+    register: replicationFactorRegister,
+    handleSubmit: handleКeplicationFactorSubmit,
+    formState: replicationFactorFormState,
+    getValues: replicationFactorgetValues,
+  } = useForm({
+    defaultValues: {
+      replicationFactor,
+    },
+  });
+
+  const validatePartitions = (data: { partitions: number }) => {
+    if (data.partitions < defaultPartitions) {
+      setPartitionsError('partitions', {
+        type: 'manual',
+        message: 'You can only increase the number of partitions!',
+      });
+    } else {
+      setPartitions(data.partitions);
+      setIsPartitionsConfirmationVisible(true);
+    }
+  };
+
+  const validateReplicationFactor = (data: { replicationFactor: number }) => {
+    setReplicationFactor(data.replicationFactor);
+    setIsReplicationFactorConfirmationVisible(true);
+  };
+
+  React.useEffect(() => {
+    if (partitionsCountIncreased) {
+      setIsPartitionsConfirmationVisible(false);
+    }
+  }, [partitionsCountIncreased]);
+
+  React.useEffect(() => {
+    if (replicationFactorUpdated) {
+      setIsReplicationFactorConfirmationVisible(false);
+    }
+  }, [replicationFactorUpdated]);
+
+  const partitionsSubmit = () => {
+    updateTopicPartitionsCount(
+      clusterName,
+      topicName,
+      partitionsGetValues('partitions')
+    );
+  };
+  const replicationFactorSubmit = () => {
+    updateTopicReplicationFactor(
+      clusterName,
+      topicName,
+      replicationFactorgetValues('replicationFactor')
+    );
+  };
+  return (
+    <div className="box">
+      <h4 className="title is-5 has-text-danger mb-5">Danger Zone</h4>
+      <div className="is-flex is-flex-direction-column">
+        <form
+          onSubmit={handlePartitionsSubmit(validatePartitions)}
+          className="columns mb-0"
+        >
+          <div className="column is-three-quarters">
+            <label className="label" htmlFor="partitions">
+              Number of partitions *
+            </label>
+            <input
+              className="input"
+              type="number"
+              id="partitions"
+              placeholder="Number of partitions"
+              {...partitionsRegister('partitions', {
+                required: 'Partiotions are required',
+              })}
+            />
+          </div>
+          <div className="column is-flex is-align-items-flex-end">
+            <input
+              type="submit"
+              className="button is-danger"
+              disabled={!partitionsFormState.isDirty}
+              data-testid="partitionsSubmit"
+            />
+          </div>
+        </form>
+        <p className="help is-danger mt-0 mb-4">
+          <ErrorMessage errors={partitionsFormState.errors} name="partitions" />
+        </p>
+        <ConfirmationModal
+          isOpen={isPartitionsConfirmationVisible}
+          onCancel={() => setIsPartitionsConfirmationVisible(false)}
+          onConfirm={partitionsSubmit}
+        >
+          Are you sure you want to increase the number of partitions? Do it only
+          if you 100% know what you are doing!
+        </ConfirmationModal>
+
+        <form
+          onSubmit={handleКeplicationFactorSubmit(validateReplicationFactor)}
+          className="columns"
+        >
+          <div className="column is-three-quarters">
+            <label className="label" htmlFor="replicationFactor">
+              Replication Factor *
+            </label>
+            <input
+              id="replicationFactor"
+              className="input"
+              type="number"
+              placeholder="Replication Factor"
+              {...replicationFactorRegister('replicationFactor', {
+                required: 'Replication Factor are required',
+              })}
+            />
+          </div>
+          <div className="column is-flex is-align-items-flex-end">
+            <input
+              type="submit"
+              className="button is-danger"
+              disabled={!replicationFactorFormState.isDirty}
+              data-testid="replicationFactorSubmit"
+            />
+          </div>
+        </form>
+        <p className="help is-danger mt-0">
+          <ErrorMessage
+            errors={replicationFactorFormState.errors}
+            name="replicationFactor"
+          />
+        </p>
+        <ConfirmationModal
+          isOpen={isReplicationFactorConfirmationVisible}
+          onCancel={() => setIsReplicationFactorConfirmationVisible(false)}
+          onConfirm={replicationFactorSubmit}
+        >
+          Are you sure you want to update the replication factor?
+        </ConfirmationModal>
+      </div>
+    </div>
+  );
+};
+
+export default DangerZone;

+ 50 - 0
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZoneContainer.ts

@@ -0,0 +1,50 @@
+import { connect } from 'react-redux';
+import { RootState, ClusterName, TopicName } from 'redux/interfaces';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+import {
+  updateTopicPartitionsCount,
+  updateTopicReplicationFactor,
+} from 'redux/actions';
+import {
+  getTopicPartitionsCountIncreased,
+  getTopicReplicationFactorUpdated,
+} from 'redux/reducers/topics/selectors';
+
+import DangerZone from './DangerZone';
+
+interface RouteProps {
+  clusterName: ClusterName;
+  topicName: TopicName;
+}
+
+type OwnProps = {
+  defaultPartitions: number;
+  defaultReplicationFactor: number;
+};
+
+const mapStateToProps = (
+  state: RootState,
+  {
+    match: {
+      params: { topicName, clusterName },
+    },
+    defaultPartitions,
+    defaultReplicationFactor,
+  }: OwnProps & RouteComponentProps<RouteProps>
+) => ({
+  clusterName,
+  topicName,
+  defaultPartitions,
+  defaultReplicationFactor,
+  partitionsCountIncreased: getTopicPartitionsCountIncreased(state),
+  replicationFactorUpdated: getTopicReplicationFactorUpdated(state),
+});
+
+const mapDispatchToProps = {
+  updateTopicPartitionsCount,
+  updateTopicReplicationFactor,
+};
+
+export default withRouter(
+  connect(mapStateToProps, mapDispatchToProps)(DangerZone)
+);

+ 26 - 10
kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx

@@ -13,6 +13,8 @@ import TopicForm from 'components/Topics/shared/Form/TopicForm';
 import { clusterTopicPath } from 'lib/paths';
 import { useHistory } from 'react-router';
 
+import DangerZoneContainer from './DangerZoneContainer';
+
 interface Props {
   clusterName: ClusterName;
   topicName: TopicName;
@@ -25,6 +27,11 @@ interface Props {
     topicName: TopicName,
     form: TopicFormDataRaw
   ) => void;
+  updateTopicPartitionsCount: (
+    clusterName: string,
+    topicname: string,
+    partitions: number
+  ) => void;
 }
 
 const DEFAULTS = {
@@ -112,17 +119,26 @@ const Edit: React.FC<Props> = ({
   };
 
   return (
-    <div className="box">
-      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
-      <FormProvider {...methods}>
-        <TopicForm
-          topicName={topicName}
-          config={config}
-          isSubmitting={isSubmitting}
-          isEditing
-          onSubmit={methods.handleSubmit(onSubmit)}
+    <div>
+      <div className="box">
+        <FormProvider {...methods}>
+          <TopicForm
+            topicName={topicName}
+            config={config}
+            isSubmitting={isSubmitting}
+            isEditing
+            onSubmit={methods.handleSubmit(onSubmit)}
+          />
+        </FormProvider>
+      </div>
+      {topic && (
+        <DangerZoneContainer
+          defaultPartitions={defaultValues.partitions}
+          defaultReplicationFactor={
+            defaultValues.replicationFactor || DEFAULTS.replicationFactor
+          }
         />
-      </FormProvider>
+      )}
     </div>
   );
 };

+ 68 - 0
kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/DangerZone.spec.tsx

@@ -0,0 +1,68 @@
+import React from 'react';
+import DangerZone, { Props } from 'components/Topics/Topic/Edit/DangerZone';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+
+const setupWrapper = (props?: Partial<Props>) => (
+  <DangerZone
+    clusterName="testCluster"
+    topicName="testTopic"
+    defaultPartitions={3}
+    defaultReplicationFactor={3}
+    partitionsCountIncreased={false}
+    replicationFactorUpdated={false}
+    updateTopicPartitionsCount={jest.fn()}
+    updateTopicReplicationFactor={jest.fn()}
+    {...props}
+  />
+);
+
+describe('DangerZone', () => {
+  it('is rendered properly', () => {
+    const component = render(setupWrapper());
+    expect(component.baseElement).toMatchSnapshot();
+  });
+
+  it('calls updateTopicPartitionsCount', async () => {
+    const mockUpdateTopicPartitionsCount = jest.fn();
+    const component = render(
+      setupWrapper({
+        updateTopicPartitionsCount: mockUpdateTopicPartitionsCount,
+      })
+    );
+
+    const input = screen.getByLabelText('Number of partitions *');
+    fireEvent.input(input, {
+      target: {
+        value: 4,
+      },
+    });
+    fireEvent.submit(screen.getByTestId('partitionsSubmit'));
+    await waitFor(() => {
+      expect(component.baseElement).toMatchSnapshot();
+      fireEvent.click(screen.getByText('Confirm'));
+      expect(mockUpdateTopicPartitionsCount).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  it('calls updateTopicReplicationFactor', async () => {
+    const mockUpdateTopicReplicationFactor = jest.fn();
+    const component = render(
+      setupWrapper({
+        updateTopicReplicationFactor: mockUpdateTopicReplicationFactor,
+      })
+    );
+
+    const input = screen.getByLabelText('Replication Factor *');
+    fireEvent.input(input, {
+      target: {
+        value: 4,
+      },
+    });
+    fireEvent.submit(screen.getByTestId('replicationFactorSubmit'));
+    await waitFor(() => {
+      expect(component.baseElement).toMatchSnapshot();
+      fireEvent.click(screen.getByText('Confirm'));
+      expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1);
+    });
+  });
+});

+ 531 - 0
kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/__snapshots__/DangerZone.spec.tsx.snap

@@ -0,0 +1,531 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DangerZone calls updateTopicPartitionsCount 1`] = `
+<body>
+  <div>
+    <div
+      class="box"
+    >
+      <h4
+        class="title is-5 has-text-danger mb-5"
+      >
+        Danger Zone
+      </h4>
+      <div
+        class="is-flex is-flex-direction-column"
+      >
+        <form
+          class="columns mb-0"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="partitions"
+            >
+              Number of partitions *
+            </label>
+            <input
+              class="input"
+              id="partitions"
+              name="partitions"
+              placeholder="Number of partitions"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="partitionsSubmit"
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0 mb-4"
+        />
+        <form
+          class="columns"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="replicationFactor"
+            >
+              Replication Factor *
+            </label>
+            <input
+              class="input"
+              id="replicationFactor"
+              name="replicationFactor"
+              placeholder="Replication Factor"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="replicationFactorSubmit"
+              disabled=""
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0"
+        />
+      </div>
+    </div>
+  </div>
+</body>
+`;
+
+exports[`DangerZone calls updateTopicPartitionsCount 2`] = `
+<body>
+  <div>
+    <div
+      class="box"
+    >
+      <h4
+        class="title is-5 has-text-danger mb-5"
+      >
+        Danger Zone
+      </h4>
+      <div
+        class="is-flex is-flex-direction-column"
+      >
+        <form
+          class="columns mb-0"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="partitions"
+            >
+              Number of partitions *
+            </label>
+            <input
+              class="input"
+              id="partitions"
+              name="partitions"
+              placeholder="Number of partitions"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="partitionsSubmit"
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0 mb-4"
+        />
+        <div
+          class="modal is-active"
+        >
+          <div
+            aria-hidden="true"
+            class="modal-background"
+          />
+          <div
+            class="modal-card"
+          >
+            <header
+              class="modal-card-head"
+            >
+              <p
+                class="modal-card-title"
+              >
+                Confirm the action
+              </p>
+              <button
+                aria-label="close"
+                class="delete"
+                type="button"
+              />
+            </header>
+            <section
+              class="modal-card-body"
+            >
+              Are you sure you want to increase the number of partitions? Do it only if you 100% know what you are doing!
+            </section>
+            <footer
+              class="modal-card-foot is-justify-content-flex-end"
+            >
+              <button
+                class="button is-danger"
+                type="button"
+              >
+                Confirm
+              </button>
+              <button
+                class="button"
+                type="button"
+              >
+                Cancel
+              </button>
+            </footer>
+          </div>
+        </div>
+        <form
+          class="columns"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="replicationFactor"
+            >
+              Replication Factor *
+            </label>
+            <input
+              class="input"
+              id="replicationFactor"
+              name="replicationFactor"
+              placeholder="Replication Factor"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="replicationFactorSubmit"
+              disabled=""
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0"
+        />
+      </div>
+    </div>
+  </div>
+</body>
+`;
+
+exports[`DangerZone calls updateTopicReplicationFactor 1`] = `
+<body>
+  <div>
+    <div
+      class="box"
+    >
+      <h4
+        class="title is-5 has-text-danger mb-5"
+      >
+        Danger Zone
+      </h4>
+      <div
+        class="is-flex is-flex-direction-column"
+      >
+        <form
+          class="columns mb-0"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="partitions"
+            >
+              Number of partitions *
+            </label>
+            <input
+              class="input"
+              id="partitions"
+              name="partitions"
+              placeholder="Number of partitions"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="partitionsSubmit"
+              disabled=""
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0 mb-4"
+        />
+        <form
+          class="columns"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="replicationFactor"
+            >
+              Replication Factor *
+            </label>
+            <input
+              class="input"
+              id="replicationFactor"
+              name="replicationFactor"
+              placeholder="Replication Factor"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="replicationFactorSubmit"
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0"
+        />
+      </div>
+    </div>
+  </div>
+</body>
+`;
+
+exports[`DangerZone calls updateTopicReplicationFactor 2`] = `
+<body>
+  <div>
+    <div
+      class="box"
+    >
+      <h4
+        class="title is-5 has-text-danger mb-5"
+      >
+        Danger Zone
+      </h4>
+      <div
+        class="is-flex is-flex-direction-column"
+      >
+        <form
+          class="columns mb-0"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="partitions"
+            >
+              Number of partitions *
+            </label>
+            <input
+              class="input"
+              id="partitions"
+              name="partitions"
+              placeholder="Number of partitions"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="partitionsSubmit"
+              disabled=""
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0 mb-4"
+        />
+        <form
+          class="columns"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="replicationFactor"
+            >
+              Replication Factor *
+            </label>
+            <input
+              class="input"
+              id="replicationFactor"
+              name="replicationFactor"
+              placeholder="Replication Factor"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="replicationFactorSubmit"
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0"
+        />
+        <div
+          class="modal is-active"
+        >
+          <div
+            aria-hidden="true"
+            class="modal-background"
+          />
+          <div
+            class="modal-card"
+          >
+            <header
+              class="modal-card-head"
+            >
+              <p
+                class="modal-card-title"
+              >
+                Confirm the action
+              </p>
+              <button
+                aria-label="close"
+                class="delete"
+                type="button"
+              />
+            </header>
+            <section
+              class="modal-card-body"
+            >
+              Are you sure you want to update the replication factor?
+            </section>
+            <footer
+              class="modal-card-foot is-justify-content-flex-end"
+            >
+              <button
+                class="button is-danger"
+                type="button"
+              >
+                Confirm
+              </button>
+              <button
+                class="button"
+                type="button"
+              >
+                Cancel
+              </button>
+            </footer>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</body>
+`;
+
+exports[`DangerZone is rendered properly 1`] = `
+<body>
+  <div>
+    <div
+      class="box"
+    >
+      <h4
+        class="title is-5 has-text-danger mb-5"
+      >
+        Danger Zone
+      </h4>
+      <div
+        class="is-flex is-flex-direction-column"
+      >
+        <form
+          class="columns mb-0"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="partitions"
+            >
+              Number of partitions *
+            </label>
+            <input
+              class="input"
+              id="partitions"
+              name="partitions"
+              placeholder="Number of partitions"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="partitionsSubmit"
+              disabled=""
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0 mb-4"
+        />
+        <form
+          class="columns"
+        >
+          <div
+            class="column is-three-quarters"
+          >
+            <label
+              class="label"
+              for="replicationFactor"
+            >
+              Replication Factor *
+            </label>
+            <input
+              class="input"
+              id="replicationFactor"
+              name="replicationFactor"
+              placeholder="Replication Factor"
+              type="number"
+            />
+          </div>
+          <div
+            class="column is-flex is-align-items-flex-end"
+          >
+            <input
+              class="button is-danger"
+              data-testid="replicationFactorSubmit"
+              disabled=""
+              type="submit"
+            />
+          </div>
+        </form>
+        <p
+          class="help is-danger mt-0"
+        />
+      </div>
+    </div>
+  </div>
+</body>
+`;

+ 20 - 18
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx

@@ -32,7 +32,7 @@ const TopicForm: React.FC<Props> = ({
       <fieldset disabled={isSubmitting}>
         <fieldset disabled={isEditing}>
           <div className="columns">
-            <div className="column is-three-quarters">
+            <div className={`column ${isEditing ? '' : 'is-three-quarters'}`}>
               <label className="label">Topic Name *</label>
               <input
                 className="input"
@@ -52,26 +52,28 @@ const TopicForm: React.FC<Props> = ({
               </p>
             </div>
 
-            <div className="column">
-              <label className="label">Number of partitions *</label>
-              <input
-                className="input"
-                type="number"
-                placeholder="Number of partitions"
-                defaultValue="1"
-                {...register('partitions', {
-                  required: 'Number of partitions is required.',
-                })}
-              />
-              <p className="help is-danger">
-                <ErrorMessage errors={errors} name="partitions" />
-              </p>
-            </div>
+            {!isEditing && (
+              <div className="column">
+                <label className="label">Number of partitions *</label>
+                <input
+                  className="input"
+                  type="number"
+                  placeholder="Number of partitions"
+                  defaultValue="1"
+                  {...register('partitions', {
+                    required: 'Number of partitions is required.',
+                  })}
+                />
+                <p className="help is-danger">
+                  <ErrorMessage errors={errors} name="partitions" />
+                </p>
+              </div>
+            )}
           </div>
         </fieldset>
 
         <div className="columns">
-          <fieldset disabled={isEditing}>
+          {!isEditing && (
             <div className="column">
               <label className="label">Replication Factor *</label>
               <input
@@ -87,7 +89,7 @@ const TopicForm: React.FC<Props> = ({
                 <ErrorMessage errors={errors} name="replicationFactor" />
               </p>
             </div>
-          </fieldset>
+          )}
 
           <div className="column">
             <label className="label">Min In Sync Replicas *</label>

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

@@ -3,6 +3,8 @@ 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 { FailurePayload } from 'redux/interfaces';
+import { getResponse } from 'lib/errorHandling';
 
 const store = mockStoreCreator;
 
@@ -132,4 +134,82 @@ describe('Thunks', () => {
       }
     });
   });
+
+  describe('increasing partitions count', () => {
+    it('calls updateTopicPartitionsCountAction.success on success', async () => {
+      fetchMock.patchOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
+        { totalPartitionsCount: 4, topicName }
+      );
+      await store.dispatch(
+        thunks.updateTopicPartitionsCount(clusterName, topicName, 4)
+      );
+      expect(store.getActions()).toEqual([
+        actions.updateTopicPartitionsCountAction.request(),
+        actions.updateTopicPartitionsCountAction.success(),
+      ]);
+    });
+
+    it('calls updateTopicPartitionsCountAction.failure on failure', async () => {
+      fetchMock.patchOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/partitions`,
+        404
+      );
+      try {
+        await store.dispatch(
+          thunks.updateTopicPartitionsCount(clusterName, topicName, 4)
+        );
+      } catch (error) {
+        const response = await getResponse(error);
+        const alert: FailurePayload = {
+          subject: ['topic-partitions', topicName].join('-'),
+          title: `Topic ${topicName} partitions count increase failed`,
+          response,
+        };
+        expect(store.getActions()).toEqual([
+          actions.updateTopicPartitionsCountAction.request(),
+          actions.updateTopicPartitionsCountAction.failure({ alert }),
+        ]);
+      }
+    });
+  });
+
+  describe('updating replication factor', () => {
+    it('calls updateTopicReplicationFactorAction.success on success', async () => {
+      fetchMock.patchOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/replications`,
+        { totalReplicationFactor: 4, topicName }
+      );
+      await store.dispatch(
+        thunks.updateTopicReplicationFactor(clusterName, topicName, 4)
+      );
+      expect(store.getActions()).toEqual([
+        actions.updateTopicReplicationFactorAction.request(),
+        actions.updateTopicReplicationFactorAction.success(),
+      ]);
+    });
+
+    it('calls updateTopicReplicationFactorAction.failure on failure', async () => {
+      fetchMock.patchOnce(
+        `/api/clusters/${clusterName}/topics/${topicName}/replications`,
+        404
+      );
+      try {
+        await store.dispatch(
+          thunks.updateTopicReplicationFactor(clusterName, topicName, 4)
+        );
+      } catch (error) {
+        const response = await getResponse(error);
+        const alert: FailurePayload = {
+          subject: ['topic-replication-factor', topicName].join('-'),
+          title: `Topic ${topicName} replication factor change failed`,
+          response,
+        };
+        expect(store.getActions()).toEqual([
+          actions.updateTopicReplicationFactorAction.request(),
+          actions.updateTopicReplicationFactorAction.failure({ alert }),
+        ]);
+      }
+    });
+  });
 });

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

@@ -253,3 +253,15 @@ export const fetchTopicConsumerGroupsAction = createAsyncAction(
   'GET_TOPIC_CONSUMER_GROUPS__SUCCESS',
   'GET_TOPIC_CONSUMER_GROUPS__FAILURE'
 )<undefined, TopicsState, undefined>();
+
+export const updateTopicPartitionsCountAction = createAsyncAction(
+  'UPDATE_PARTITIONS__REQUEST',
+  'UPDATE_PARTITIONS__SUCCESS',
+  'UPDATE_PARTITIONS__FAILURE'
+)<undefined, undefined, { alert?: FailurePayload }>();
+
+export const updateTopicReplicationFactorAction = createAsyncAction(
+  'UPDATE_REPLICATION_FACTOR__REQUEST',
+  'UPDATE_REPLICATION_FACTOR__SUCCESS',
+  'UPDATE_REPLICATION_FACTOR__FAILURE'
+)<undefined, undefined, { alert?: FailurePayload }>();

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

@@ -341,3 +341,55 @@ export const fetchTopicConsumerGroups =
       dispatch(actions.fetchTopicConsumerGroupsAction.failure());
     }
   };
+
+export const updateTopicPartitionsCount =
+  (
+    clusterName: ClusterName,
+    topicName: TopicName,
+    partitions: number
+  ): PromiseThunkResult =>
+  async (dispatch) => {
+    dispatch(actions.updateTopicPartitionsCountAction.request());
+    try {
+      await topicsApiClient.increaseTopicPartitions({
+        clusterName,
+        topicName,
+        partitionsIncrease: { totalPartitionsCount: partitions },
+      });
+      dispatch(actions.updateTopicPartitionsCountAction.success());
+    } catch (error) {
+      const response = await getResponse(error);
+      const alert: FailurePayload = {
+        subject: ['topic-partitions', topicName].join('-'),
+        title: `Topic ${topicName} partitions count increase failed`,
+        response,
+      };
+      dispatch(actions.updateTopicPartitionsCountAction.failure({ alert }));
+    }
+  };
+
+export const updateTopicReplicationFactor =
+  (
+    clusterName: ClusterName,
+    topicName: TopicName,
+    replicationFactor: number
+  ): PromiseThunkResult =>
+  async (dispatch) => {
+    dispatch(actions.updateTopicReplicationFactorAction.request());
+    try {
+      await topicsApiClient.changeReplicationFactor({
+        clusterName,
+        topicName,
+        replicationFactorChange: { totalReplicationFactor: replicationFactor },
+      });
+      dispatch(actions.updateTopicReplicationFactorAction.success());
+    } catch (error) {
+      const response = await getResponse(error);
+      const alert: FailurePayload = {
+        subject: ['topic-replication-factor', topicName].join('-'),
+        title: `Topic ${topicName} replication factor change failed`,
+        response,
+      };
+      dispatch(actions.updateTopicReplicationFactorAction.failure({ alert }));
+    }
+  };

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

@@ -25,6 +25,11 @@ const getTopicMessagesFetchingStatus =
 const getTopicConfigFetchingStatus = createFetchingSelector('GET_TOPIC_CONFIG');
 const getTopicCreationStatus = createFetchingSelector('POST_TOPIC');
 const getTopicUpdateStatus = createFetchingSelector('PATCH_TOPIC');
+const getPartitionsCountIncreaseStatus =
+  createFetchingSelector('UPDATE_PARTITIONS');
+const getReplicationFactorUpdateStatus = createFetchingSelector(
+  'UPDATE_REPLICATION_FACTOR'
+);
 
 export const getAreTopicsFetching = createSelector(
   getTopicListFetchingStatus,
@@ -66,6 +71,16 @@ export const getTopicUpdated = createSelector(
   (status) => status === 'fetched'
 );
 
+export const getTopicPartitionsCountIncreased = createSelector(
+  getPartitionsCountIncreaseStatus,
+  (status) => status === 'fetched'
+);
+
+export const getTopicReplicationFactorUpdated = createSelector(
+  getReplicationFactorUpdateStatus,
+  (status) => status === 'fetched'
+);
+
 export const getTopicList = createSelector(
   getAreTopicsFetched,
   getAllNames,

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

@@ -1,6 +1,7 @@
 import { configure } from 'enzyme';
 import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
 import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom';
 
 configure({ adapter: new Adapter() });