From 8da350ea38531c2259b45c94e3645bec1d008a62 Mon Sep 17 00:00:00 2001 From: Alexander Krivonosov <31561808+GneyHabub@users.noreply.github.com> Date: Sat, 24 Jul 2021 18:00:29 +0300 Subject: [PATCH] Allow increasing the number of partitions and updating of the replication factor (#544) --- kafka-ui-react-app/package-lock.json | 111 ++-- kafka-ui-react-app/package.json | 3 +- .../Topics/Topic/Edit/DangerZone.tsx | 200 +++++++ .../Topics/Topic/Edit/DangerZoneContainer.ts | 50 ++ .../src/components/Topics/Topic/Edit/Edit.tsx | 36 +- .../Topic/Edit/__tests__/DangerZone.spec.tsx | 68 +++ .../__snapshots__/DangerZone.spec.tsx.snap | 531 ++++++++++++++++++ .../Topics/shared/Form/TopicForm.tsx | 38 +- .../actions/__test__/thunks/topics.spec.ts | 80 +++ .../src/redux/actions/actions.ts | 12 + .../src/redux/actions/thunks/topics.ts | 52 ++ .../src/redux/reducers/topics/selectors.ts | 15 + kafka-ui-react-app/src/setupTests.ts | 1 + 13 files changed, 1128 insertions(+), 69 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone.tsx create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZoneContainer.ts create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/DangerZone.spec.tsx create mode 100644 kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/__snapshots__/DangerZone.spec.tsx.snap diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index a40a5e4837..907f44538b 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/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" } diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index d3d768531a..f3d539ab80 100644 --- a/kafka-ui-react-app/package.json +++ b/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", diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone.tsx new file mode 100644 index 0000000000..ee6f75c16d --- /dev/null +++ b/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 = ({ + clusterName, + topicName, + defaultPartitions, + defaultReplicationFactor, + partitionsCountIncreased, + replicationFactorUpdated, + updateTopicPartitionsCount, + updateTopicReplicationFactor, +}) => { + const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] = + React.useState(false); + const [ + isReplicationFactorConfirmationVisible, + setIsReplicationFactorConfirmationVisible, + ] = React.useState(false); + const [partitions, setPartitions] = React.useState(defaultPartitions); + const [replicationFactor, setReplicationFactor] = React.useState( + 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 ( +
+

Danger Zone

+
+
+
+ + +
+
+ +
+
+

+ +

+ 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! + + +
+
+ + +
+
+ +
+
+

+ +

+ setIsReplicationFactorConfirmationVisible(false)} + onConfirm={replicationFactorSubmit} + > + Are you sure you want to update the replication factor? + +
+
+ ); +}; + +export default DangerZone; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZoneContainer.ts b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZoneContainer.ts new file mode 100644 index 0000000000..5bd6f0ade5 --- /dev/null +++ b/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 +) => ({ + clusterName, + topicName, + defaultPartitions, + defaultReplicationFactor, + partitionsCountIncreased: getTopicPartitionsCountIncreased(state), + replicationFactorUpdated: getTopicReplicationFactorUpdated(state), +}); + +const mapDispatchToProps = { + updateTopicPartitionsCount, + updateTopicReplicationFactor, +}; + +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(DangerZone) +); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx index 5972cd716d..b6728eaf91 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx +++ b/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 = ({ }; return ( -
- {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - +
+ + + +
+ {topic && ( + -
+ )}
); }; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/DangerZone.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/DangerZone.spec.tsx new file mode 100644 index 0000000000..857a0eafd5 --- /dev/null +++ b/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) => ( + +); + +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); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/__snapshots__/DangerZone.spec.tsx.snap b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/__snapshots__/DangerZone.spec.tsx.snap new file mode 100644 index 0000000000..dbd90936ad --- /dev/null +++ b/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`] = ` + +
+
+

+ Danger Zone +

+
+
+
+ + +
+
+ +
+
+

+

+
+ + +
+
+ +
+
+

+

+
+
+ +`; + +exports[`DangerZone calls updateTopicPartitionsCount 2`] = ` + +
+
+

+ Danger Zone +

+
+
+
+ + +
+
+ +
+
+

+

+
+
+ +`; + +exports[`DangerZone calls updateTopicReplicationFactor 1`] = ` + +
+
+

+ Danger Zone +

+
+
+
+ + +
+
+ +
+
+

+

+
+ + +
+
+ +
+
+

+

+
+
+ +`; + +exports[`DangerZone calls updateTopicReplicationFactor 2`] = ` + +
+
+

+ Danger Zone +

+
+
+
+ + +
+
+ +
+
+

+

+
+ + +
+
+ +
+
+

+

+
+
+ +`; + +exports[`DangerZone is rendered properly 1`] = ` + +
+
+

+ Danger Zone +

+
+
+
+ + +
+
+ +
+
+

+

+
+ + +
+
+ +
+
+

+

+
+
+ +`; diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx index c6d223e97e..eb52d147a0 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx @@ -32,7 +32,7 @@ const TopicForm: React.FC = ({
-
+
= ({

-
- - -

- -

-
+ {!isEditing && ( +
+ + +

+ +

+
+ )}
-
+ {!isEditing && (
= ({

-
+ )}
diff --git a/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts b/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts index 7a1a2db89a..b92b7e08ce 100644 --- a/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts +++ b/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 }), + ]); + } + }); + }); }); diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts index fa451edb6f..4788e801db 100644 --- a/kafka-ui-react-app/src/redux/actions/actions.ts +++ b/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' )(); + +export const updateTopicPartitionsCountAction = createAsyncAction( + 'UPDATE_PARTITIONS__REQUEST', + 'UPDATE_PARTITIONS__SUCCESS', + 'UPDATE_PARTITIONS__FAILURE' +)(); + +export const updateTopicReplicationFactorAction = createAsyncAction( + 'UPDATE_REPLICATION_FACTOR__REQUEST', + 'UPDATE_REPLICATION_FACTOR__SUCCESS', + 'UPDATE_REPLICATION_FACTOR__FAILURE' +)(); diff --git a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts index be6ad93f61..61594a3c63 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts +++ b/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 })); + } + }; diff --git a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts b/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts index 99075fd531..ff9883039d 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/selectors.ts +++ b/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, diff --git a/kafka-ui-react-app/src/setupTests.ts b/kafka-ui-react-app/src/setupTests.ts index 5c70aaaca6..f97deac682 100644 --- a/kafka-ui-react-app/src/setupTests.ts +++ b/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() });