瀏覽代碼

react router migration (#2045)

* remove withRouter HOC from FiltersContainer

* remove withRouter HOC from Topics DetailsContainer

* remove withRouter HOC from Topics TopicsConsumerGroupsContainer

* withRouter HOC from Topics TopicsConsumerGroupsContainer

* minor code refactor in the Details spec

* Routes code modifications to refactor strings representation to functions

* Settings and TopicsConsumer removal of HOC with Router

* Remove withRouter HOC from Overview file

* Remove withRouter HOC from Edit file

* replace Router path with functions instead of strings

* delete CustomParamsContainer and use the simple component in the TopicForm

* remove HOC from DangerZone container

* Remove withRouter HOC from Connect pages like Config , Overview , Tasks

* Remove withRouter HOC from Connect pages like Actions, Details, Edit, New

* Refactor Kafka Connect Codes

* Refactor Topics pages

* Remove HOC from Diff component and minor code refactor

* Route component migration into children instead of renderProps or component param in App Component

* Route component migration into children instead of renderProps or component param in Cluster Component

* Route component migration into children instead of renderProps or component param in Topics Component

* Route component migration into children instead of renderProps or component param in Topic Component

* Route component migration into children instead of renderProps or component param in Topic Component

* minor bug fix in the Overview selector spread

* change Router from component Render to child render
in ConsumerGroups page

* change Router from component Render to child render
in Schemas page

* change Router from component Render to child render
in KsqlDb page

* change Router from component Render to child render
in Connect page

* change Router from component Render to child render
in Connect Details page

* Overview Details styling code modifications

* All written path to paths with functions

* Route Parameters code fix with functions and params with variables

* Updating BreadCrumb Route

* Refactor Redirects

* WIP React Router v6 migration

* Remove unused imports from the file

* Make KsqlDb pages work with relative Routes

* WIP Make Connect pages work and fix the Schema page testing problem

* transforming consumer groups into relative path router

* Transform Topics pages into relative routes

* Transform Topic pages into relative routes

* Minor changes in Connect and KsqlDb test suites relative routes

* Minor changes in Connect and KsqlDb test suites relative routes

* change the Details into relative Routes

* Topics List naviagtion and caching issue fixed in tests suites

* Topic New Naviagation issue fix + tests suites

* Details navigate migrating into relative paths

* Send Message Submit Naviagttion with tests suites

* Topic Edit pages with working routes navigation

* Topic Details and ResetOffsets Pages tests suites and navigations

* Messages Table Tests suites

* BreadCrumbs Routes fixes

* ClusterMenu and Links styling minor code modifications

* ClusterMenu and Links styling minor code modifications

* Minor Code modifications

* Fix Lintter Problems

* fix Code Smells

* create custom useParams hook

* Adding Path tests

* minor code refactors

* Fix the Button Component redundant Props + transforming routes to relative

* Fix linter issues
Mgrdich 3 年之前
父節點
當前提交
71ac16357b
共有 100 個文件被更改,包括 1520 次插入1664 次删除
  1. 31 86
      kafka-ui-react-app/package-lock.json
  2. 1 1
      kafka-ui-react-app/package.json
  3. 13 7
      kafka-ui-react-app/src/components/App.tsx
  4. 3 3
      kafka-ui-react-app/src/components/Brokers/Brokers.tsx
  5. 4 5
      kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx
  6. 67 35
      kafka-ui-react-app/src/components/Cluster/Cluster.tsx
  7. 41 19
      kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx
  8. 49 46
      kafka-ui-react-app/src/components/Connect/Connect.tsx
  9. 8 11
      kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
  10. 1 4
      kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts
  11. 54 69
      kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx
  12. 4 8
      kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx
  13. 1 2
      kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts
  14. 25 24
      kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx
  15. 17 39
      kafka-ui-react-app/src/components/Connect/Details/Details.tsx
  16. 1 4
      kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts
  17. 1 2
      kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts
  18. 4 8
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx
  19. 2 5
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts
  20. 11 14
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx
  21. 1 2
      kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts
  22. 17 19
      kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx
  23. 86 72
      kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx
  24. 10 11
      kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx
  25. 1 2
      kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts
  26. 12 17
      kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx
  27. 4 5
      kafka-ui-react-app/src/components/Connect/List/List.tsx
  28. 1 4
      kafka-ui-react-app/src/components/Connect/List/ListItem.tsx
  29. 6 9
      kafka-ui-react-app/src/components/Connect/New/New.tsx
  30. 1 2
      kafka-ui-react-app/src/components/Connect/New/NewContainer.ts
  31. 5 8
      kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx
  32. 34 18
      kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx
  33. 28 14
      kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx
  34. 9 13
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  35. 8 11
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx
  36. 9 13
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx
  37. 9 10
      kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx
  38. 16 22
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx
  39. 9 10
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx
  40. 1 1
      kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx
  41. 3 3
      kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx
  42. 1 4
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx
  43. 10 21
      kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx
  44. 19 9
      kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx
  45. 5 6
      kafka-ui-react-app/src/components/KsqlDb/List/List.tsx
  46. 5 10
      kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx
  47. 5 10
      kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx
  48. 3 2
      kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx
  49. 4 5
      kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx
  50. 35 8
      kafka-ui-react-app/src/components/KsqlDb/__test__/KsqlDb.spec.tsx
  51. 0 5
      kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx
  52. 2 5
      kafka-ui-react-app/src/components/Nav/ClusterMenuItem.tsx
  53. 4 5
      kafka-ui-react-app/src/components/Nav/Nav.styled.ts
  54. 1 1
      kafka-ui-react-app/src/components/Nav/Nav.tsx
  55. 6 19
      kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx
  56. 10 12
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  57. 4 5
      kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx
  58. 21 25
      kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx
  59. 2 19
      kafka-ui-react-app/src/components/Schemas/Diff/DiffContainer.ts
  60. 51 20
      kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx
  61. 6 6
      kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx
  62. 7 6
      kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx
  63. 4 3
      kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx
  64. 4 5
      kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx
  65. 4 5
      kafka-ui-react-app/src/components/Schemas/List/List.tsx
  66. 1 1
      kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx
  67. 4 5
      kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx
  68. 6 5
      kafka-ui-react-app/src/components/Schemas/New/New.tsx
  69. 4 5
      kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx
  70. 41 28
      kafka-ui-react-app/src/components/Schemas/Schemas.tsx
  71. 34 14
      kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx
  72. 3 3
      kafka-ui-react-app/src/components/Topics/List/List.styled.ts
  73. 21 14
      kafka-ui-react-app/src/components/Topics/List/List.tsx
  74. 1 1
      kafka-ui-react-app/src/components/Topics/List/TopicsTableCells.tsx
  75. 64 57
      kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx
  76. 8 10
      kafka-ui-react-app/src/components/Topics/New/New.tsx
  77. 50 72
      kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx
  78. 12 10
      kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups.tsx
  79. 7 27
      kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroupsContainer.ts
  80. 42 17
      kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/__test__/TopicConsumerGroups.spec.tsx
  81. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts
  82. 101 102
      kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx
  83. 4 29
      kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts
  84. 13 10
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
  85. 3 24
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/FiltersContainer.ts
  86. 5 4
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx
  87. 2 10
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx
  88. 12 13
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx
  89. 25 19
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx
  90. 1 26
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts
  91. 96 80
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx
  92. 11 13
      kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx
  93. 4 27
      kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts
  94. 43 38
      kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.spec.tsx
  95. 43 47
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx
  96. 4 4
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZone.tsx
  97. 3 19
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts
  98. 14 16
      kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx
  99. 13 17
      kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx
  100. 3 22
      kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx

+ 31 - 86
kafka-ui-react-app/package-lock.json

@@ -36,7 +36,7 @@
         "react-multi-select-component": "^4.0.6",
         "react-redux": "^7.2.6",
         "react-router": "^5.2.0",
-        "react-router-dom": "^5.3.1",
+        "react-router-dom": "^6.3.0",
         "redux": "^4.1.1",
         "redux-thunk": "^2.3.0",
         "sass": "^1.43.4",
@@ -16158,7 +16158,6 @@
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
       "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
-      "dev": true,
       "dependencies": {
         "@babel/runtime": "^7.7.6"
       }
@@ -24856,11 +24855,11 @@
       }
     },
     "node_modules/react-router": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
-      "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
+      "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
       "dependencies": {
-        "@babel/runtime": "^7.1.2",
+        "@babel/runtime": "^7.12.13",
         "history": "^4.9.0",
         "hoist-non-react-statics": "^3.1.0",
         "loose-envify": "^1.3.1",
@@ -24876,53 +24875,27 @@
       }
     },
     "node_modules/react-router-dom": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz",
-      "integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
+      "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
       "dependencies": {
-        "@babel/runtime": "^7.12.13",
-        "history": "^4.9.0",
-        "loose-envify": "^1.3.1",
-        "prop-types": "^15.6.2",
-        "react-router": "5.3.1",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0"
+        "history": "^5.2.0",
+        "react-router": "6.3.0"
       },
       "peerDependencies": {
-        "react": ">=15"
-      }
-    },
-    "node_modules/react-router-dom/node_modules/history": {
-      "version": "4.10.1",
-      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
-      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
-      "dependencies": {
-        "@babel/runtime": "^7.1.2",
-        "loose-envify": "^1.2.0",
-        "resolve-pathname": "^3.0.0",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0",
-        "value-equal": "^1.0.1"
+        "react": ">=16.8",
+        "react-dom": ">=16.8"
       }
     },
     "node_modules/react-router-dom/node_modules/react-router": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz",
-      "integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
+      "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
       "dependencies": {
-        "@babel/runtime": "^7.12.13",
-        "history": "^4.9.0",
-        "hoist-non-react-statics": "^3.1.0",
-        "loose-envify": "^1.3.1",
-        "mini-create-react-context": "^0.4.0",
-        "path-to-regexp": "^1.7.0",
-        "prop-types": "^15.6.2",
-        "react-is": "^16.6.0",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0"
+        "history": "^5.2.0"
       },
       "peerDependencies": {
-        "react": ">=15"
+        "react": ">=16.8"
       }
     },
     "node_modules/react-router/node_modules/history": {
@@ -40798,7 +40771,6 @@
       "version": "5.3.0",
       "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
       "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
-      "dev": true,
       "requires": {
         "@babel/runtime": "^7.7.6"
       }
@@ -47310,11 +47282,11 @@
       "dev": true
     },
     "react-router": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
-      "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
+      "integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
       "requires": {
-        "@babel/runtime": "^7.1.2",
+        "@babel/runtime": "^7.12.13",
         "history": "^4.9.0",
         "hoist-non-react-statics": "^3.1.0",
         "loose-envify": "^1.3.1",
@@ -47342,47 +47314,20 @@
       }
     },
     "react-router-dom": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.1.tgz",
-      "integrity": "sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
+      "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
       "requires": {
-        "@babel/runtime": "^7.12.13",
-        "history": "^4.9.0",
-        "loose-envify": "^1.3.1",
-        "prop-types": "^15.6.2",
-        "react-router": "5.3.1",
-        "tiny-invariant": "^1.0.2",
-        "tiny-warning": "^1.0.0"
+        "history": "^5.2.0",
+        "react-router": "6.3.0"
       },
       "dependencies": {
-        "history": {
-          "version": "4.10.1",
-          "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
-          "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
-          "requires": {
-            "@babel/runtime": "^7.1.2",
-            "loose-envify": "^1.2.0",
-            "resolve-pathname": "^3.0.0",
-            "tiny-invariant": "^1.0.2",
-            "tiny-warning": "^1.0.0",
-            "value-equal": "^1.0.1"
-          }
-        },
         "react-router": {
-          "version": "5.3.1",
-          "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.1.tgz",
-          "integrity": "sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ==",
-          "requires": {
-            "@babel/runtime": "^7.12.13",
-            "history": "^4.9.0",
-            "hoist-non-react-statics": "^3.1.0",
-            "loose-envify": "^1.3.1",
-            "mini-create-react-context": "^0.4.0",
-            "path-to-regexp": "^1.7.0",
-            "prop-types": "^15.6.2",
-            "react-is": "^16.6.0",
-            "tiny-invariant": "^1.0.2",
-            "tiny-warning": "^1.0.0"
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
+          "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
+          "requires": {
+            "history": "^5.2.0"
           }
         }
       }

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

@@ -32,7 +32,7 @@
     "react-multi-select-component": "^4.0.6",
     "react-redux": "^7.2.6",
     "react-router": "^5.2.0",
-    "react-router-dom": "^5.3.1",
+    "react-router-dom": "^6.3.0",
     "redux": "^4.1.1",
     "redux-thunk": "^2.3.0",
     "sass": "^1.43.4",

+ 13 - 7
kafka-ui-react-app/src/components/App.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
-import { Switch, Route, useLocation } from 'react-router-dom';
+import { Routes, Route, useLocation } from 'react-router-dom';
 import { GIT_TAG, GIT_COMMIT } from 'lib/constants';
+import { clusterPath, getNonExactPath } from 'lib/paths';
 import Nav from 'components/Nav/Nav';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import Dashboard from 'components/Dashboard/Dashboard';
@@ -81,14 +82,19 @@ const App: React.FC = () => {
             aria-label="Overlay"
           />
           {areClustersFulfilled ? (
-            <Switch>
+            <Routes>
+              {['/', '/ui', '/ui/clusters'].map((path) => (
+                <Route
+                  key="Home" // optional: avoid full re-renders on route changes
+                  path={path}
+                  element={<Dashboard />}
+                />
+              ))}
               <Route
-                exact
-                path={['/', '/ui', '/ui/clusters']}
-                component={Dashboard}
+                path={getNonExactPath(clusterPath())}
+                element={<ClusterPage />}
               />
-              <Route path="/ui/clusters/:clusterName" component={ClusterPage} />
-            </Switch>
+            </Routes>
           ) : (
             <PageLoader />
           )}

+ 3 - 3
kafka-ui-react-app/src/components/Brokers/Brokers.tsx

@@ -1,22 +1,22 @@
 import React from 'react';
-import { ClusterName } from 'redux/interfaces';
 import useInterval from 'lib/hooks/useInterval';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
-import { useParams } from 'react-router-dom';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import { Table } from 'components/common/table/Table/Table.styled';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import * as Metrics from 'components/common/Metrics';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import { ClusterNameRoute } from 'lib/paths';
 import {
   fetchBrokers,
   fetchClusterStats,
   selectStats,
 } from 'redux/reducers/brokers/brokersSlice';
+import useAppParams from 'lib/hooks/useAppParams';
 
 const Brokers: React.FC = () => {
   const dispatch = useAppDispatch();
-  const { clusterName } = useParams<{ clusterName: ClusterName }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
   const {
     brokerCount,
     activeControllers,

+ 4 - 5
kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx

@@ -1,8 +1,7 @@
 import React from 'react';
 import Brokers from 'components/Brokers/Brokers';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { screen, waitFor } from '@testing-library/dom';
-import { Route } from 'react-router-dom';
 import { clusterBrokersPath } from 'lib/paths';
 import fetchMock from 'fetch-mock';
 import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures';
@@ -18,11 +17,11 @@ describe('Brokers Component', () => {
 
   const renderComponent = () =>
     render(
-      <Route path={clusterBrokersPath(':clusterName')}>
+      <WithRoute path={clusterBrokersPath()}>
         <Brokers />
-      </Route>,
+      </WithRoute>,
       {
-        pathname: clusterBrokersPath(clusterName),
+        initialEntries: [clusterBrokersPath(clusterName)],
       }
     );
 

+ 67 - 35
kafka-ui-react-app/src/components/Cluster/Cluster.tsx

@@ -1,19 +1,22 @@
 import React from 'react';
 import { useSelector } from 'react-redux';
-import { Switch, Redirect, useParams } from 'react-router-dom';
+import { Routes, Navigate, Route, Outlet } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { ClusterFeaturesEnum } from 'generated-sources';
 import {
   getClustersFeatures,
   getClustersReadonlyStatus,
 } from 'redux/reducers/clusters/clustersSlice';
 import {
-  clusterBrokersPath,
-  clusterConnectorsPath,
-  clusterConnectsPath,
-  clusterConsumerGroupsPath,
-  clusterKsqlDbPath,
-  clusterSchemasPath,
-  clusterTopicsPath,
+  clusterBrokerRelativePath,
+  clusterConnectorsRelativePath,
+  clusterConnectsRelativePath,
+  clusterConsumerGroupsRelativePath,
+  clusterKsqlDbRelativePath,
+  ClusterNameRoute,
+  clusterSchemasRelativePath,
+  clusterTopicsRelativePath,
+  getNonExactPath,
 } from 'lib/paths';
 import Topics from 'components/Topics/Topics';
 import Schemas from 'components/Schemas/Schemas';
@@ -27,7 +30,7 @@ import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider';
 
 const Cluster: React.FC = () => {
-  const { clusterName } = useParams<{ clusterName: string }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
   const isReadOnly = useSelector(getClustersReadonlyStatus(clusterName));
   const features = useSelector(getClustersFeatures(clusterName));
 
@@ -61,48 +64,77 @@ const Cluster: React.FC = () => {
     <BreadcrumbProvider>
       <Breadcrumb />
       <ClusterContext.Provider value={contextValue}>
-        <Switch>
-          <BreadcrumbRoute
-            path={clusterBrokersPath(':clusterName')}
-            component={Brokers}
+        <Routes>
+          <Route
+            path={getNonExactPath(clusterBrokerRelativePath)}
+            element={
+              <BreadcrumbRoute>
+                <Brokers />
+              </BreadcrumbRoute>
+            }
           />
-          <BreadcrumbRoute
-            path={clusterTopicsPath(':clusterName')}
-            component={Topics}
+          <Route
+            path={getNonExactPath(clusterTopicsRelativePath)}
+            element={
+              <BreadcrumbRoute>
+                <Topics />
+              </BreadcrumbRoute>
+            }
           />
-          <BreadcrumbRoute
-            path={clusterConsumerGroupsPath(':clusterName')}
-            component={ConsumersGroups}
+          <Route
+            path={getNonExactPath(clusterConsumerGroupsRelativePath)}
+            element={
+              <BreadcrumbRoute>
+                <ConsumersGroups />
+              </BreadcrumbRoute>
+            }
           />
           {hasSchemaRegistryConfigured && (
-            <BreadcrumbRoute
-              path={clusterSchemasPath(':clusterName')}
-              component={Schemas}
+            <Route
+              path={getNonExactPath(clusterSchemasRelativePath)}
+              element={
+                <BreadcrumbRoute>
+                  <Schemas />
+                </BreadcrumbRoute>
+              }
             />
           )}
           {hasKafkaConnectConfigured && (
-            <BreadcrumbRoute
-              path={clusterConnectsPath(':clusterName')}
-              component={Connect}
+            <Route
+              path={getNonExactPath(clusterConnectsRelativePath)}
+              element={
+                <BreadcrumbRoute>
+                  <Connect />
+                </BreadcrumbRoute>
+              }
             />
           )}
           {hasKafkaConnectConfigured && (
-            <BreadcrumbRoute
-              path={clusterConnectorsPath(':clusterName')}
-              component={Connect}
+            <Route
+              path={getNonExactPath(clusterConnectorsRelativePath)}
+              element={
+                <BreadcrumbRoute>
+                  <Connect />
+                </BreadcrumbRoute>
+              }
             />
           )}
           {hasKsqlDbConfigured && (
-            <BreadcrumbRoute
-              path={clusterKsqlDbPath(':clusterName')}
-              component={KsqlDb}
+            <Route
+              path={getNonExactPath(clusterKsqlDbRelativePath)}
+              element={
+                <BreadcrumbRoute>
+                  <KsqlDb />
+                </BreadcrumbRoute>
+              }
             />
           )}
-          <Redirect
-            from="/ui/clusters/:clusterName"
-            to="/ui/clusters/:clusterName/brokers"
+          <Route
+            path="/"
+            element={<Navigate to={clusterBrokerRelativePath} replace />}
           />
-        </Switch>
+        </Routes>
+        <Outlet />
       </ClusterContext.Provider>
     </BreadcrumbProvider>
   );

+ 41 - 19
kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx

@@ -1,51 +1,71 @@
 import React from 'react';
-import { Route } from 'react-router-dom';
 import { ClusterFeaturesEnum } from 'generated-sources';
 import { store } from 'redux/store';
 import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
 import Cluster from 'components/Cluster/Cluster';
 import { fetchClusters } from 'redux/reducers/clusters/clustersSlice';
 import { screen } from '@testing-library/react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import {
   clusterBrokersPath,
   clusterConnectsPath,
   clusterConsumerGroupsPath,
   clusterKsqlDbPath,
+  clusterPath,
   clusterSchemasPath,
   clusterTopicsPath,
 } from 'lib/paths';
 
-jest.mock('components/Topics/Topics', () => () => <div>Topics</div>);
-jest.mock('components/Schemas/Schemas', () => () => <div>Schemas</div>);
-jest.mock('components/Connect/Connect', () => () => <div>Connect</div>);
-jest.mock('components/Connect/Connect', () => () => <div>Connect</div>);
-jest.mock('components/Brokers/Brokers', () => () => <div>Brokers</div>);
+const CLusterCompText = {
+  Topics: 'Topics',
+  Schemas: 'Schemas',
+  Connect: 'Connect',
+  Brokers: 'Brokers',
+  ConsumerGroups: 'ConsumerGroups',
+  KsqlDb: 'KsqlDb',
+};
+
+jest.mock('components/Topics/Topics', () => () => (
+  <div>{CLusterCompText.Topics}</div>
+));
+jest.mock('components/Schemas/Schemas', () => () => (
+  <div>{CLusterCompText.Schemas}</div>
+));
+jest.mock('components/Connect/Connect', () => () => (
+  <div>{CLusterCompText.Connect}</div>
+));
+jest.mock('components/Brokers/Brokers', () => () => (
+  <div>{CLusterCompText.Brokers}</div>
+));
 jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => (
-  <div>ConsumerGroups</div>
+  <div>{CLusterCompText.ConsumerGroups}</div>
+));
+jest.mock('components/KsqlDb/KsqlDb', () => () => (
+  <div>{CLusterCompText.KsqlDb}</div>
 ));
-jest.mock('components/KsqlDb/KsqlDb', () => () => <div>KsqlDb</div>);
 
 describe('Cluster', () => {
   const renderComponent = (pathname: string) =>
     render(
-      <Route path="/ui/clusters/:clusterName">
+      <WithRoute path={`${clusterPath()}/*`}>
         <Cluster />
-      </Route>,
-      { pathname, store }
+      </WithRoute>,
+      { initialEntries: [pathname], store }
     );
 
   it('renders Brokers', () => {
     renderComponent(clusterBrokersPath('second'));
-    expect(screen.getByText('Brokers')).toBeInTheDocument();
+    expect(screen.getByText(CLusterCompText.Brokers)).toBeInTheDocument();
   });
   it('renders Topics', () => {
     renderComponent(clusterTopicsPath('second'));
-    expect(screen.getByText('Topics')).toBeInTheDocument();
+    expect(screen.getByText(CLusterCompText.Topics)).toBeInTheDocument();
   });
   it('renders ConsumerGroups', () => {
     renderComponent(clusterConsumerGroupsPath('second'));
-    expect(screen.getByText('ConsumerGroups')).toBeInTheDocument();
+    expect(
+      screen.getByText(CLusterCompText.ConsumerGroups)
+    ).toBeInTheDocument();
   });
 
   describe('configured features', () => {
@@ -62,7 +82,9 @@ describe('Cluster', () => {
         )
       );
       renderComponent(clusterSchemasPath('second'));
-      expect(screen.queryByText('Schemas')).not.toBeInTheDocument();
+      expect(
+        screen.queryByText(CLusterCompText.Schemas)
+      ).not.toBeInTheDocument();
     });
     it('renders Schemas if SCHEMA_REGISTRY is configured', async () => {
       store.dispatch(
@@ -77,7 +99,7 @@ describe('Cluster', () => {
         )
       );
       renderComponent(clusterSchemasPath(onlineClusterPayload.name));
-      expect(screen.getByText('Schemas')).toBeInTheDocument();
+      expect(screen.getByText(CLusterCompText.Schemas)).toBeInTheDocument();
     });
     it('renders Connect if KAFKA_CONNECT is configured', async () => {
       store.dispatch(
@@ -92,7 +114,7 @@ describe('Cluster', () => {
         )
       );
       renderComponent(clusterConnectsPath(onlineClusterPayload.name));
-      expect(screen.getByText('Connect')).toBeInTheDocument();
+      expect(screen.getByText(CLusterCompText.Connect)).toBeInTheDocument();
     });
     it('renders KSQL if KSQL_DB is configured', async () => {
       store.dispatch(
@@ -107,7 +129,7 @@ describe('Cluster', () => {
         )
       );
       renderComponent(clusterKsqlDbPath(onlineClusterPayload.name));
-      expect(screen.getByText('KsqlDb')).toBeInTheDocument();
+      expect(screen.getByText(CLusterCompText.KsqlDb)).toBeInTheDocument();
     });
   });
 });

+ 49 - 46
kafka-ui-react-app/src/components/Connect/Connect.tsx

@@ -1,12 +1,12 @@
 import React from 'react';
-import { Switch, Redirect } from 'react-router-dom';
+import { Navigate, Routes, Route } from 'react-router-dom';
 import {
-  clusterConnectorsPath,
-  clusterConnectsPath,
-  clusterConnectorNewPath,
-  clusterConnectConnectorPath,
-  clusterConnectConnectorEditPath,
-  clusterConnectConnectorsPath,
+  RouteParams,
+  clusterConnectConnectorEditRelativePath,
+  clusterConnectConnectorRelativePath,
+  clusterConnectConnectorsRelativePath,
+  clusterConnectorNewRelativePath,
+  getNonExactPath,
 } from 'lib/paths';
 import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 
@@ -16,45 +16,48 @@ import DetailsContainer from './Details/DetailsContainer';
 import EditContainer from './Edit/EditContainer';
 
 const Connect: React.FC = () => (
-  <div>
-    <Switch>
-      <BreadcrumbRoute
-        exact
-        path={clusterConnectorsPath(':clusterName')}
-        component={ListContainer}
-      />
-      <BreadcrumbRoute
-        exact
-        path={clusterConnectorNewPath(':clusterName')}
-        component={NewContainer}
-      />
-      <BreadcrumbRoute
-        exact
-        path={clusterConnectConnectorEditPath(
-          ':clusterName',
-          ':connectName',
-          ':connectorName'
-        )}
-        component={EditContainer}
-      />
-      <BreadcrumbRoute
-        path={clusterConnectConnectorPath(
-          ':clusterName',
-          ':connectName',
-          ':connectorName'
-        )}
-        component={DetailsContainer}
-      />
-      <Redirect
-        from={clusterConnectConnectorsPath(':clusterName', ':connectName')}
-        to={clusterConnectorsPath(':clusterName')}
-      />
-      <Redirect
-        from={`${clusterConnectsPath(':clusterName')}/:connectName`}
-        to={clusterConnectorsPath(':clusterName')}
-      />
-    </Switch>
-  </div>
+  <Routes>
+    <Route
+      index
+      element={
+        <BreadcrumbRoute>
+          <ListContainer />
+        </BreadcrumbRoute>
+      }
+    />
+    <Route
+      path={clusterConnectorNewRelativePath}
+      element={
+        <BreadcrumbRoute>
+          <NewContainer />
+        </BreadcrumbRoute>
+      }
+    />
+    <Route
+      path={clusterConnectConnectorEditRelativePath}
+      element={
+        <BreadcrumbRoute>
+          <EditContainer />
+        </BreadcrumbRoute>
+      }
+    />
+    <Route
+      path={getNonExactPath(clusterConnectConnectorRelativePath)}
+      element={
+        <BreadcrumbRoute>
+          <DetailsContainer />
+        </BreadcrumbRoute>
+      }
+    />
+    <Route
+      path={clusterConnectConnectorsRelativePath}
+      element={<Navigate to="/" replace />}
+    />
+    <Route
+      path={RouteParams.connectName}
+      element={<Navigate to="/" replace />}
+    />
+  </Routes>
 );
 
 export default Connect;

+ 8 - 11
kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx

@@ -1,21 +1,17 @@
 import React from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { ConnectorState, ConnectorAction } from 'generated-sources';
 import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
 import {
   clusterConnectConnectorEditPath,
   clusterConnectorsPath,
+  RouterParamsClusterConnectConnector,
 } from 'lib/paths';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import styled from 'styled-components';
 import { Button } from 'components/common/Button/Button';
 
-interface RouterParams {
-  clusterName: ClusterName;
-  connectName: ConnectName;
-  connectorName: ConnectorName;
-}
-
 const ConnectorActionsWrapperStyled = styled.div`
   display: flex;
   gap: 8px;
@@ -63,9 +59,11 @@ const Actions: React.FC<ActionsProps> = ({
   resumeConnector,
   isConnectorActionRunning,
 }) => {
-  const { clusterName, connectName, connectorName } = useParams<RouterParams>();
+  const { clusterName, connectName, connectorName } =
+    useAppParams<RouterParamsClusterConnectConnector>();
+
+  const navigate = useNavigate();
 
-  const history = useHistory();
   const [
     isDeleteConnectorConfirmationVisible,
     setIsDeleteConnectorConfirmationVisible,
@@ -74,7 +72,7 @@ const Actions: React.FC<ActionsProps> = ({
   const deleteConnectorHandler = async () => {
     try {
       await deleteConnector({ clusterName, connectName, connectorName });
-      history.push(clusterConnectorsPath(clusterName));
+      navigate(clusterConnectorsPath(clusterName));
     } catch {
       // do not redirect
     }
@@ -175,7 +173,6 @@ const Actions: React.FC<ActionsProps> = ({
         buttonSize="M"
         buttonType="primary"
         type="button"
-        isLink
         disabled={isConnectorActionRunning}
         to={clusterConnectConnectorEditPath(
           clusterName,

+ 1 - 4
kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts

@@ -1,5 +1,4 @@
 import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
 import { RootState } from 'redux/interfaces';
 import {
   deleteConnector,
@@ -30,6 +29,4 @@ const mapDispatchToProps = {
   resumeConnector,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Actions)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(Actions);

+ 54 - 69
kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { Route } from 'react-router-dom';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths';
 import ActionsContainer from 'components/Connect/Details/Actions/ActionsContainer';
 import Actions, {
@@ -19,9 +18,7 @@ const cancelMock = jest.fn();
 
 jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
-  useHistory: () => ({
-    push: mockHistoryPush,
-  }),
+  useNavigate: () => mockHistoryPush,
 }));
 
 jest.mock(
@@ -38,6 +35,12 @@ const expectActionButtonsExists = () => {
 };
 
 describe('Actions', () => {
+  afterEach(() => {
+    mockHistoryPush.mockClear();
+    deleteConnector.mockClear();
+    cancelMock.mockClear();
+  });
+
   const actionsContainer = (props: Partial<ActionsProps> = {}) => (
     <ActionsContainer>
       <Actions
@@ -60,17 +63,13 @@ describe('Actions', () => {
   });
 
   describe('view', () => {
-    const pathname = clusterConnectConnectorPath(
-      ':clusterName',
-      ':connectName',
-      ':connectorName'
-    );
+    const pathname = clusterConnectConnectorPath();
     const clusterName = 'my-cluster';
     const connectName = 'my-connect';
     const connectorName = 'my-connector';
 
     const confirmationModal = (props: Partial<ConfirmationModalProps> = {}) => (
-      <Route path={pathname}>
+      <WithRoute path={pathname}>
         <ConfirmationModal
           onCancel={cancelMock}
           onConfirm={() =>
@@ -91,11 +90,11 @@ describe('Actions', () => {
             Confirm
           </button>
         </ConfirmationModal>
-      </Route>
+      </WithRoute>
     );
 
     const component = (props: Partial<ActionsProps> = {}) => (
-      <Route path={pathname}>
+      <WithRoute path={pathname}>
         <Actions
           deleteConnector={jest.fn()}
           isConnectorDeleting={false}
@@ -107,16 +106,14 @@ describe('Actions', () => {
           isConnectorActionRunning={false}
           {...props}
         />
-      </Route>
+      </WithRoute>
     );
 
     it('renders buttons when paused', () => {
       render(component({ connectorStatus: ConnectorState.PAUSED }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       expect(screen.getAllByRole('button').length).toEqual(6);
       expect(screen.getByText('Resume')).toBeInTheDocument();
@@ -127,11 +124,9 @@ describe('Actions', () => {
 
     it('renders buttons when failed', () => {
       render(component({ connectorStatus: ConnectorState.FAILED }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       expect(screen.getAllByRole('button').length).toEqual(5);
 
@@ -143,11 +138,9 @@ describe('Actions', () => {
 
     it('renders buttons when unassigned', () => {
       render(component({ connectorStatus: ConnectorState.UNASSIGNED }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       expect(screen.getAllByRole('button').length).toEqual(5);
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
@@ -157,11 +150,9 @@ describe('Actions', () => {
 
     it('renders buttons when running connector action', () => {
       render(component({ connectorStatus: ConnectorState.RUNNING }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       expect(screen.getAllByRole('button').length).toEqual(6);
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
@@ -172,11 +163,9 @@ describe('Actions', () => {
 
     it('opens confirmation modal when delete button clicked', () => {
       render(component({ deleteConnector }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       userEvent.click(screen.getByRole('button', { name: 'Delete' }));
 
@@ -187,11 +176,9 @@ describe('Actions', () => {
 
     it('closes when cancel button clicked', () => {
       render(confirmationModal({ isOpen: true }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
       userEvent.click(cancelBtn);
@@ -200,11 +187,9 @@ describe('Actions', () => {
 
     it('calls deleteConnector when confirm button clicked', () => {
       render(confirmationModal({ isOpen: true }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       const confirmBtn = screen.getByRole('button', { name: 'Confirm' });
       userEvent.click(confirmBtn);
@@ -218,11 +203,9 @@ describe('Actions', () => {
 
     it('redirects after delete', async () => {
       render(confirmationModal({ isOpen: true }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       const confirmBtn = screen.getByRole('button', { name: 'Confirm' });
       userEvent.click(confirmBtn);
@@ -235,11 +218,9 @@ describe('Actions', () => {
     it('calls restartConnector when restart button clicked', () => {
       const restartConnector = jest.fn();
       render(component({ restartConnector }), {
-        pathname: clusterConnectConnectorPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorPath(clusterName, connectName, connectorName),
+        ],
       });
       userEvent.click(
         screen.getByRole('button', { name: 'Restart Connector' })
@@ -260,11 +241,13 @@ describe('Actions', () => {
           pauseConnector,
         }),
         {
-          pathname: clusterConnectConnectorPath(
-            clusterName,
-            connectName,
-            connectorName
-          ),
+          initialEntries: [
+            clusterConnectConnectorPath(
+              clusterName,
+              connectName,
+              connectorName
+            ),
+          ],
         }
       );
       userEvent.click(screen.getByRole('button', { name: 'Pause' }));
@@ -284,11 +267,13 @@ describe('Actions', () => {
           resumeConnector,
         }),
         {
-          pathname: clusterConnectConnectorPath(
-            clusterName,
-            connectName,
-            connectorName
-          ),
+          initialEntries: [
+            clusterConnectConnectorPath(
+              clusterName,
+              connectName,
+              connectorName
+            ),
+          ],
         }
       );
       userEvent.click(screen.getByRole('button', { name: 'Resume' }));

+ 4 - 8
kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { useParams } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import {
   ClusterName,
   ConnectName,
@@ -9,12 +9,7 @@ import {
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import Editor from 'components/common/Editor/Editor';
 import styled from 'styled-components';
-
-interface RouterParams {
-  clusterName: ClusterName;
-  connectName: ConnectName;
-  connectorName: ConnectorName;
-}
+import { RouterParamsClusterConnectConnector } from 'lib/paths';
 
 export interface ConfigProps {
   fetchConfig(payload: {
@@ -35,7 +30,8 @@ const Config: React.FC<ConfigProps> = ({
   isConfigFetching,
   config,
 }) => {
-  const { clusterName, connectName, connectorName } = useParams<RouterParams>();
+  const { clusterName, connectName, connectorName } =
+    useAppParams<RouterParamsClusterConnectConnector>();
 
   React.useEffect(() => {
     fetchConfig({ clusterName, connectName, connectorName });

+ 1 - 2
kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts

@@ -1,5 +1,4 @@
 import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
 import { RootState } from 'redux/interfaces';
 import { fetchConnectorConfig } from 'redux/reducers/connect/connectSlice';
 import {
@@ -18,4 +17,4 @@ const mapDispatchToProps = {
   fetchConfig: fetchConnectorConfig,
 };
 
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Config));
+export default connect(mapStateToProps, mapDispatchToProps)(Config);

+ 25 - 24
kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
-import { Route } from 'react-router-dom';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConnectConnectorConfigPath } from 'lib/paths';
 import Config, { ConfigProps } from 'components/Connect/Details/Config/Config';
 import { connector } from 'redux/reducers/connect/__test__/fixtures';
@@ -9,44 +8,44 @@ import { screen } from '@testing-library/dom';
 jest.mock('components/common/Editor/Editor', () => 'mock-Editor');
 
 describe('Config', () => {
-  const pathname = clusterConnectConnectorConfigPath(
-    ':clusterName',
-    ':connectName',
-    ':connectorName'
-  );
+  const pathname = clusterConnectConnectorConfigPath();
   const clusterName = 'my-cluster';
   const connectName = 'my-connect';
   const connectorName = 'my-connector';
 
   const component = (props: Partial<ConfigProps> = {}) => (
-    <Route path={pathname}>
+    <WithRoute path={pathname}>
       <Config
         fetchConfig={jest.fn()}
         isConfigFetching={false}
         config={connector.config}
         {...props}
       />
-    </Route>
+    </WithRoute>
   );
 
   it('to be in the document when fetching config', () => {
     render(component({ isConfigFetching: true }), {
-      pathname: clusterConnectConnectorConfigPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
+      initialEntries: [
+        clusterConnectConnectorConfigPath(
+          clusterName,
+          connectName,
+          connectorName
+        ),
+      ],
     });
     expect(screen.getByRole('progressbar')).toBeInTheDocument();
   });
 
   it('is empty when no config', () => {
     const { container } = render(component({ config: null }), {
-      pathname: clusterConnectConnectorConfigPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
+      initialEntries: [
+        clusterConnectConnectorConfigPath(
+          clusterName,
+          connectName,
+          connectorName
+        ),
+      ],
     });
     expect(container).toBeEmptyDOMElement();
   });
@@ -54,11 +53,13 @@ describe('Config', () => {
   it('fetches config on mount', () => {
     const fetchConfig = jest.fn();
     render(component({ fetchConfig }), {
-      pathname: clusterConnectConnectorConfigPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
+      initialEntries: [
+        clusterConnectConnectorConfigPath(
+          clusterName,
+          connectName,
+          connectorName
+        ),
+      ],
     });
     expect(fetchConfig).toHaveBeenCalledTimes(1);
     expect(fetchConfig).toHaveBeenCalledWith({

+ 17 - 39
kafka-ui-react-app/src/components/Connect/Details/Details.tsx

@@ -1,11 +1,15 @@
 import React from 'react';
-import { NavLink, Route, Switch, useParams } from 'react-router-dom';
+import { NavLink, Route, Routes } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { Connector, Task } from 'generated-sources';
 import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
 import {
   clusterConnectConnectorConfigPath,
+  clusterConnectConnectorConfigRelativePath,
   clusterConnectConnectorPath,
   clusterConnectConnectorTasksPath,
+  clusterConnectConnectorTasksRelativePath,
+  RouterParamsClusterConnectConnector,
 } from 'lib/paths';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import Navbar from 'components/common/Navigation/Navbar.styled';
@@ -16,12 +20,6 @@ import TasksContainer from './Tasks/TasksContainer';
 import ConfigContainer from './Config/ConfigContainer';
 import ActionsContainer from './Actions/ActionsContainer';
 
-interface RouterParams {
-  clusterName: ClusterName;
-  connectName: ConnectName;
-  connectorName: ConnectorName;
-}
-
 export interface DetailsProps {
   fetchConnector(payload: {
     clusterName: ClusterName;
@@ -46,7 +44,8 @@ const Details: React.FC<DetailsProps> = ({
   areTasksFetching,
   connector,
 }) => {
-  const { clusterName, connectName, connectorName } = useParams<RouterParams>();
+  const { clusterName, connectName, connectorName } =
+    useAppParams<RouterParamsClusterConnectConnector>();
 
   React.useEffect(() => {
     fetchConnector({ clusterName, connectName, connectorName });
@@ -69,68 +68,47 @@ const Details: React.FC<DetailsProps> = ({
       </PageHeading>
       <Navbar role="navigation">
         <NavLink
-          exact
           to={clusterConnectConnectorPath(
             clusterName,
             connectName,
             connectorName
           )}
-          activeClassName="is-active"
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
         >
           Overview
         </NavLink>
         <NavLink
-          exact
           to={clusterConnectConnectorTasksPath(
             clusterName,
             connectName,
             connectorName
           )}
-          activeClassName="is-active"
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
         >
           Tasks
         </NavLink>
         <NavLink
-          exact
           to={clusterConnectConnectorConfigPath(
             clusterName,
             connectName,
             connectorName
           )}
-          activeClassName="is-active"
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
         >
           Config
         </NavLink>
       </Navbar>
-      <Switch>
+      <Routes>
+        <Route index element={<OverviewContainer />} />
         <Route
-          exact
-          path={clusterConnectConnectorTasksPath(
-            ':clusterName',
-            ':connectName',
-            ':connectorName'
-          )}
-          component={TasksContainer}
+          path={clusterConnectConnectorTasksRelativePath}
+          element={<TasksContainer />}
         />
         <Route
-          exact
-          path={clusterConnectConnectorConfigPath(
-            ':clusterName',
-            ':connectName',
-            ':connectorName'
-          )}
-          component={ConfigContainer}
-        />
-        <Route
-          exact
-          path={clusterConnectConnectorPath(
-            ':clusterName',
-            ':connectName',
-            ':connectorName'
-          )}
-          component={OverviewContainer}
+          path={clusterConnectConnectorConfigRelativePath}
+          element={<ConfigContainer />}
         />
-      </Switch>
+      </Routes>
     </div>
   );
 };

+ 1 - 4
kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts

@@ -1,5 +1,4 @@
 import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
 import { RootState } from 'redux/interfaces';
 import {
   fetchConnector,
@@ -26,6 +25,4 @@ const mapDispatchToProps = {
   fetchTasks: fetchConnectorTasks,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Details)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(Details);

+ 1 - 2
kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts

@@ -1,5 +1,4 @@
 import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
 import { RootState } from 'redux/interfaces';
 import {
   getConnector,
@@ -15,4 +14,4 @@ const mapStateToProps = (state: RootState) => ({
   failedTasksCount: getConnectorFailedTasksCount(state),
 });
 
-export default withRouter(connect(mapStateToProps)(Overview));
+export default connect(mapStateToProps)(Overview);

+ 4 - 8
kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { useParams } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { Task, TaskId } from 'generated-sources';
 import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
 import Dropdown from 'components/common/Dropdown/Dropdown';
@@ -7,12 +7,7 @@ import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import * as C from 'components/common/Tag/Tag.styled';
 import getTagColor from 'components/common/Tag/getTagColor';
-
-interface RouterParams {
-  clusterName: ClusterName;
-  connectName: ConnectName;
-  connectorName: ConnectorName;
-}
+import { RouterParamsClusterConnectConnector } from 'lib/paths';
 
 export interface ListItemProps {
   task: Task;
@@ -25,7 +20,8 @@ export interface ListItemProps {
 }
 
 const ListItem: React.FC<ListItemProps> = ({ task, restartTask }) => {
-  const { clusterName, connectName, connectorName } = useParams<RouterParams>();
+  const { clusterName, connectName, connectorName } =
+    useAppParams<RouterParamsClusterConnectConnector>();
 
   const restartTaskHandler = async () => {
     await restartTask({

+ 2 - 5
kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts

@@ -1,12 +1,11 @@
 import { connect } from 'react-redux';
-import { RouteComponentProps, withRouter } from 'react-router-dom';
 import { Task } from 'generated-sources';
 import { RootState } from 'redux/interfaces';
 import { restartConnectorTask } from 'redux/reducers/connect/connectSlice';
 
 import ListItem from './ListItem';
 
-interface OwnProps extends RouteComponentProps {
+interface OwnProps {
   task: Task;
 }
 
@@ -18,6 +17,4 @@ const mapDispatchToProps = {
   restartTask: restartConnectorTask,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(ListItem)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(ListItem);

+ 11 - 14
kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx

@@ -1,19 +1,14 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConnectConnectorTasksPath } from 'lib/paths';
 import ListItem, {
   ListItemProps,
 } from 'components/Connect/Details/Tasks/ListItem/ListItem';
 import { tasks } from 'redux/reducers/connect/__test__/fixtures';
-import { Route } from 'react-router-dom';
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 
-const pathname = clusterConnectConnectorTasksPath(
-  ':clusterName',
-  ':connectName',
-  ':connectorName'
-);
+const pathname = clusterConnectConnectorTasksPath();
 const clusterName = 'my-cluster';
 const connectName = 'my-connect';
 const connectorName = 'my-connector';
@@ -22,19 +17,21 @@ const task = tasks[0];
 
 const renderComponent = (props: ListItemProps = { task, restartTask }) => {
   return render(
-    <Route path={pathname}>
+    <WithRoute path={pathname}>
       <table>
         <tbody>
           <ListItem {...props} />
         </tbody>
       </table>
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterConnectConnectorTasksPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
+      initialEntries: [
+        clusterConnectConnectorTasksPath(
+          clusterName,
+          connectName,
+          connectorName
+        ),
+      ],
     }
   );
 };

+ 1 - 2
kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts

@@ -1,5 +1,4 @@
 import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
 import { RootState } from 'redux/interfaces';
 import { fetchConnectorTasks } from 'redux/reducers/connect/connectSlice';
 import {
@@ -18,4 +17,4 @@ const mapDispatchToProps = {
   fetchTasks: fetchConnectorTasks,
 };
 
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Tasks));
+export default connect(mapStateToProps, mapDispatchToProps)(Tasks);

+ 17 - 19
kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx

@@ -1,10 +1,9 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConnectConnectorTasksPath } from 'lib/paths';
 import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer';
 import Tasks, { TasksProps } from 'components/Connect/Details/Tasks/Tasks';
 import { tasks } from 'redux/reducers/connect/__test__/fixtures';
-import { Route } from 'react-router-dom';
 import { screen } from '@testing-library/dom';
 
 jest.mock(
@@ -19,28 +18,25 @@ describe('Tasks', () => {
   });
 
   describe('view', () => {
-    const pathname = clusterConnectConnectorTasksPath(
-      ':clusterName',
-      ':connectName',
-      ':connectorName'
-    );
     const clusterName = 'my-cluster';
     const connectName = 'my-connect';
     const connectorName = 'my-connector';
 
     const setupWrapper = (props: Partial<TasksProps> = {}) => (
-      <Route path={pathname}>
+      <WithRoute path={clusterConnectConnectorTasksPath()}>
         <Tasks areTasksFetching={false} tasks={tasks} {...props} />
-      </Route>
+      </WithRoute>
     );
 
     it('to be in the document when fetching tasks', () => {
       render(setupWrapper({ areTasksFetching: true }), {
-        pathname: clusterConnectConnectorTasksPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorTasksPath(
+            clusterName,
+            connectName,
+            connectorName
+          ),
+        ],
       });
       expect(screen.getByRole('progressbar')).toBeInTheDocument();
       expect(screen.queryByRole('table')).not.toBeInTheDocument();
@@ -48,11 +44,13 @@ describe('Tasks', () => {
 
     it('to be in the document when no tasks', () => {
       render(setupWrapper({ tasks: [] }), {
-        pathname: clusterConnectConnectorTasksPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorTasksPath(
+            clusterName,
+            connectName,
+            connectorName
+          ),
+        ],
       });
       expect(screen.getByRole('table')).toBeInTheDocument();
       expect(screen.getByText('No tasks found')).toBeInTheDocument();

+ 86 - 72
kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx

@@ -1,100 +1,89 @@
 import React from 'react';
-import { Route } from 'react-router-dom';
-import { render } from 'lib/testHelpers';
-import { clusterConnectConnectorPath } from 'lib/paths';
+import { render, WithRoute } from 'lib/testHelpers';
+import {
+  clusterConnectConnectorConfigPath,
+  clusterConnectConnectorPath,
+  clusterConnectConnectorTasksPath,
+  getNonExactPath,
+} from 'lib/paths';
 import Details, { DetailsProps } from 'components/Connect/Details/Details';
 import { connector, tasks } from 'redux/reducers/connect/__test__/fixtures';
 import { screen } from '@testing-library/dom';
 
-jest.mock(
-  'components/Connect/Details/Overview/OverviewContainer',
-  () => 'mock-OverviewContainer'
-);
+const DetailsCompText = {
+  overview: 'OverviewContainer',
+  tasks: 'TasksContainer',
+  config: 'ConfigContainer',
+  actions: 'ActionsContainer',
+};
 
-jest.mock(
-  'components/Connect/Details/Tasks/TasksContainer',
-  () => 'mock-TasksContainer'
-);
+jest.mock('components/Connect/Details/Overview/OverviewContainer', () => () => (
+  <div>{DetailsCompText.overview}</div>
+));
 
-jest.mock(
-  'components/Connect/Details/Config/ConfigContainer',
-  () => 'mock-ConfigContainer'
-);
+jest.mock('components/Connect/Details/Tasks/TasksContainer', () => () => (
+  <div>{DetailsCompText.tasks}</div>
+));
 
-jest.mock(
-  'components/Connect/Details/Actions/ActionsContainer',
-  () => 'mock-ActionsContainer'
-);
+jest.mock('components/Connect/Details/Config/ConfigContainer', () => () => (
+  <div>{DetailsCompText.config}</div>
+));
+
+jest.mock('components/Connect/Details/Actions/ActionsContainer', () => () => (
+  <div>{DetailsCompText.actions}</div>
+));
 
 describe('Details', () => {
-  const pathname = clusterConnectConnectorPath(
-    ':clusterName',
-    ':connectName',
-    ':connectorName'
-  );
   const clusterName = 'my-cluster';
   const connectName = 'my-connect';
   const connectorName = 'my-connector';
-
-  const setupWrapper = (props: Partial<DetailsProps> = {}) => (
-    <Route path={pathname}>
-      <Details
-        fetchConnector={jest.fn()}
-        fetchTasks={jest.fn()}
-        isConnectorFetching={false}
-        areTasksFetching={false}
-        connector={connector}
-        tasks={tasks}
-        {...props}
-      />
-    </Route>
+  const defaultPath = clusterConnectConnectorPath(
+    clusterName,
+    connectName,
+    connectorName
   );
 
+  const setupWrapper = (
+    props: Partial<DetailsProps> = {},
+    path: string = defaultPath
+  ) =>
+    render(
+      <WithRoute path={getNonExactPath(clusterConnectConnectorPath())}>
+        <Details
+          fetchConnector={jest.fn()}
+          fetchTasks={jest.fn()}
+          isConnectorFetching={false}
+          areTasksFetching={false}
+          connector={connector}
+          tasks={tasks}
+          {...props}
+        />
+      </WithRoute>,
+      { initialEntries: [path] }
+    );
+
   it('renders progressbar when fetching connector', () => {
-    render(setupWrapper({ isConnectorFetching: true }), {
-      pathname: clusterConnectConnectorPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
-    });
+    setupWrapper({ isConnectorFetching: true });
 
     expect(screen.getByRole('progressbar')).toBeInTheDocument();
     expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
   });
 
   it('renders progressbar when fetching tasks', () => {
-    render(setupWrapper({ areTasksFetching: true }), {
-      pathname: clusterConnectConnectorPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
-    });
+    setupWrapper({ areTasksFetching: true });
+
     expect(screen.getByRole('progressbar')).toBeInTheDocument();
     expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
   });
 
   it('is empty when no connector', () => {
-    const { container } = render(setupWrapper({ connector: null }), {
-      pathname: clusterConnectConnectorPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
-    });
+    const { container } = setupWrapper({ connector: null });
     expect(container).toBeEmptyDOMElement();
   });
 
   it('fetches connector on mount', () => {
     const fetchConnector = jest.fn();
-    render(setupWrapper({ fetchConnector }), {
-      pathname: clusterConnectConnectorPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
-    });
+    setupWrapper({ fetchConnector });
     expect(fetchConnector).toHaveBeenCalledTimes(1);
     expect(fetchConnector).toHaveBeenCalledWith({
       clusterName,
@@ -105,13 +94,7 @@ describe('Details', () => {
 
   it('fetches tasks on mount', () => {
     const fetchTasks = jest.fn();
-    render(setupWrapper({ fetchTasks }), {
-      pathname: clusterConnectConnectorPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
-    });
+    setupWrapper({ fetchTasks });
     expect(fetchTasks).toHaveBeenCalledTimes(1);
     expect(fetchTasks).toHaveBeenCalledWith({
       clusterName,
@@ -119,4 +102,35 @@ describe('Details', () => {
       connectorName,
     });
   });
+
+  describe('Router component tests', () => {
+    it('should test if overview is rendering', () => {
+      setupWrapper({});
+      expect(screen.getByText(DetailsCompText.overview));
+    });
+
+    it('should test if tasks is rendering', () => {
+      setupWrapper(
+        {},
+        clusterConnectConnectorTasksPath(
+          clusterName,
+          connectName,
+          connectorName
+        )
+      );
+      expect(screen.getByText(DetailsCompText.tasks));
+    });
+
+    it('should test if list is rendering', () => {
+      setupWrapper(
+        {},
+        clusterConnectConnectorConfigPath(
+          clusterName,
+          connectName,
+          connectorName
+        )
+      );
+      expect(screen.getByText(DetailsCompText.config));
+    });
+  });
 });

+ 10 - 11
kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { Controller, useForm } from 'react-hook-form';
 import { ErrorMessage } from '@hookform/error-message';
 import { yupResolver } from '@hookform/resolvers/yup';
@@ -9,7 +10,10 @@ import {
   ConnectorConfig,
   ConnectorName,
 } from 'redux/interfaces';
-import { clusterConnectConnectorConfigPath } from 'lib/paths';
+import {
+  clusterConnectConnectorConfigPath,
+  RouterParamsClusterConnectConnector,
+} from 'lib/paths';
 import yup from 'lib/yupExtended';
 import Editor from 'components/common/Editor/Editor';
 import PageLoader from 'components/common/PageLoader/PageLoader';
@@ -24,12 +28,6 @@ const validationSchema = yup.object().shape({
   config: yup.string().required().isJsonObject(),
 });
 
-interface RouterParams {
-  clusterName: ClusterName;
-  connectName: ConnectName;
-  connectorName: ConnectorName;
-}
-
 interface FormValues {
   config: string;
 }
@@ -56,8 +54,9 @@ const Edit: React.FC<EditProps> = ({
   config,
   updateConfig,
 }) => {
-  const { clusterName, connectName, connectorName } = useParams<RouterParams>();
-  const history = useHistory();
+  const { clusterName, connectName, connectorName } =
+    useAppParams<RouterParamsClusterConnectConnector>();
+  const navigate = useNavigate();
   const {
     handleSubmit,
     control,
@@ -89,7 +88,7 @@ const Edit: React.FC<EditProps> = ({
       connectorConfig: JSON.parse(values.config.trim()),
     });
     if (connector) {
-      history.push(
+      navigate(
         clusterConnectConnectorConfigPath(
           clusterName,
           connectName,

+ 1 - 2
kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts

@@ -1,5 +1,4 @@
 import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
 import { RootState } from 'redux/interfaces';
 import {
   fetchConnectorConfig,
@@ -22,4 +21,4 @@ const mapDispatchToProps = {
   updateConfig: updateConnectorConfig,
 };
 
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));
+export default connect(mapStateToProps, mapDispatchToProps)(Edit);

+ 12 - 17
kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx

@@ -1,12 +1,11 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import {
   clusterConnectConnectorConfigPath,
   clusterConnectConnectorEditPath,
 } from 'lib/paths';
 import Edit, { EditProps } from 'components/Connect/Edit/Edit';
 import { connector } from 'redux/reducers/connect/__test__/fixtures';
-import { Route } from 'react-router-dom';
 import { waitFor } from '@testing-library/dom';
 import { act, fireEvent, screen } from '@testing-library/react';
 
@@ -17,24 +16,18 @@ jest.mock('components/common/Editor/Editor', () => 'mock-Editor');
 const mockHistoryPush = jest.fn();
 jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
-  useHistory: () => ({
-    push: mockHistoryPush,
-  }),
+  useNavigate: () => mockHistoryPush,
 }));
 
 describe('Edit', () => {
-  const pathname = clusterConnectConnectorEditPath(
-    ':clusterName',
-    ':connectName',
-    ':connectorName'
-  );
+  const pathname = clusterConnectConnectorEditPath();
   const clusterName = 'my-cluster';
   const connectName = 'my-connect';
   const connectorName = 'my-connector';
 
   const renderComponent = (props: Partial<EditProps> = {}) =>
     render(
-      <Route path={pathname}>
+      <WithRoute path={pathname}>
         <Edit
           fetchConfig={jest.fn()}
           isConfigFetching={false}
@@ -42,13 +35,15 @@ describe('Edit', () => {
           updateConfig={jest.fn()}
           {...props}
         />
-      </Route>,
+      </WithRoute>,
       {
-        pathname: clusterConnectConnectorEditPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
+        initialEntries: [
+          clusterConnectConnectorEditPath(
+            clusterName,
+            connectName,
+            connectorName
+          ),
+        ],
       }
     );
 

+ 4 - 5
kafka-ui-react-app/src/components/Connect/List/List.tsx

@@ -1,8 +1,8 @@
 import React from 'react';
-import { useParams } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { Connect, FullConnectorInfo } from 'generated-sources';
 import { ClusterName, ConnectorSearch } from 'redux/interfaces';
-import { clusterConnectorNewPath } from 'lib/paths';
+import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import Search from 'components/common/Search/Search';
@@ -40,7 +40,7 @@ const List: React.FC<ListProps> = ({
   setConnectorSearch,
 }) => {
   const { isReadOnly } = React.useContext(ClusterContext);
-  const { clusterName } = useParams<{ clusterName: string }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
 
   React.useEffect(() => {
     fetchConnects(clusterName);
@@ -58,10 +58,9 @@ const List: React.FC<ListProps> = ({
       <PageHeading text="Connectors">
         {!isReadOnly && (
           <Button
-            isLink
             buttonType="primary"
             buttonSize="M"
-            to={clusterConnectorNewPath(clusterName)}
+            to={clusterConnectorNewRelativePath}
           >
             Create Connector
           </Button>

+ 1 - 4
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

@@ -60,10 +60,7 @@ const ListItem: React.FC<ListItemProps> = ({
   return (
     <tr>
       <TableKeyLink>
-        <NavLink
-          exact
-          to={clusterConnectConnectorPath(clusterName, connect, name)}
-        >
+        <NavLink to={clusterConnectConnectorPath(clusterName, connect, name)}>
           {name}
         </NavLink>
       </TableKeyLink>

+ 6 - 9
kafka-ui-react-app/src/components/Connect/New/New.tsx

@@ -1,11 +1,12 @@
 import React from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { Controller, FormProvider, useForm } from 'react-hook-form';
 import { ErrorMessage } from '@hookform/error-message';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { Connect, Connector, NewConnector } from 'generated-sources';
 import { ClusterName, ConnectName } from 'redux/interfaces';
-import { clusterConnectConnectorPath } from 'lib/paths';
+import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
 import yup from 'lib/yupExtended';
 import Editor from 'components/common/Editor/Editor';
 import PageLoader from 'components/common/PageLoader/PageLoader';
@@ -23,10 +24,6 @@ const validationSchema = yup.object().shape({
   config: yup.string().required().isJsonObject(),
 });
 
-interface RouterParams {
-  clusterName: ClusterName;
-}
-
 export interface NewProps {
   fetchConnects(clusterName: ClusterName): unknown;
   areConnectsFetching: boolean;
@@ -50,8 +47,8 @@ const New: React.FC<NewProps> = ({
   connects,
   createConnector,
 }) => {
-  const { clusterName } = useParams<RouterParams>();
-  const history = useHistory();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
+  const navigate = useNavigate();
 
   const methods = useForm<FormValues>({
     mode: 'onTouched',
@@ -96,7 +93,7 @@ const New: React.FC<NewProps> = ({
     });
 
     if (connector) {
-      history.push(
+      navigate(
         clusterConnectConnectorPath(
           clusterName,
           connector.connect,

+ 1 - 2
kafka-ui-react-app/src/components/Connect/New/NewContainer.ts

@@ -1,5 +1,4 @@
 import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
 import {
   createConnector,
   fetchConnects,
@@ -22,4 +21,4 @@ const mapDispatchToProps = {
   createConnector: createConnector as unknown as NewProps['createConnector'],
 };
 
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(New));
+export default connect(mapStateToProps, mapDispatchToProps)(New);

+ 5 - 8
kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx

@@ -1,12 +1,11 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import {
   clusterConnectConnectorPath,
   clusterConnectorNewPath,
 } from 'lib/paths';
 import New, { NewProps } from 'components/Connect/New/New';
 import { connects, connector } from 'redux/reducers/connect/__test__/fixtures';
-import { Route } from 'react-router-dom';
 import { fireEvent, screen, act } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { ControllerRenderProps } from 'react-hook-form';
@@ -22,9 +21,7 @@ jest.mock(
 const mockHistoryPush = jest.fn();
 jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
-  useHistory: () => ({
-    push: mockHistoryPush,
-  }),
+  useNavigate: () => mockHistoryPush,
 }));
 
 describe('New', () => {
@@ -51,7 +48,7 @@ describe('New', () => {
 
   const renderComponent = (props: Partial<NewProps> = {}) =>
     render(
-      <Route path={clusterConnectorNewPath(':clusterName')}>
+      <WithRoute path={clusterConnectorNewPath()}>
         <New
           fetchConnects={jest.fn()}
           areConnectsFetching={false}
@@ -59,8 +56,8 @@ describe('New', () => {
           createConnector={jest.fn()}
           {...props}
         />
-      </Route>,
-      { pathname: clusterConnectorNewPath(clusterName) }
+      </WithRoute>,
+      { initialEntries: [clusterConnectorNewPath(clusterName)] }
     );
 
   it('fetches connects on mount', async () => {

+ 34 - 18
kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx

@@ -1,53 +1,68 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { screen } from '@testing-library/react';
 import Connect from 'components/Connect/Connect';
 import { store } from 'redux/store';
-import { Route } from 'react-router-dom';
 import {
   clusterConnectorsPath,
   clusterConnectorNewPath,
   clusterConnectConnectorPath,
   clusterConnectConnectorEditPath,
+  getNonExactPath,
+  clusterConnectsPath,
 } from 'lib/paths';
 
+const ConnectCompText = {
+  new: 'NewContainer',
+  list: 'ListContainer',
+  details: 'DetailsContainer',
+  edit: 'EditContainer',
+};
+
 jest.mock('components/Connect/New/NewContainer', () => () => (
-  <div>NewContainer</div>
+  <div>{ConnectCompText.new}</div>
 ));
 jest.mock('components/Connect/List/ListContainer', () => () => (
-  <div>ListContainer</div>
+  <div>{ConnectCompText.list}</div>
 ));
 jest.mock('components/Connect/Details/DetailsContainer', () => () => (
-  <div>DetailsContainer</div>
+  <div>{ConnectCompText.details}</div>
 ));
 jest.mock('components/Connect/Edit/EditContainer', () => () => (
-  <div>EditContainer</div>
+  <div>{ConnectCompText.edit}</div>
 ));
 
 describe('Connect', () => {
-  const renderComponent = (pathname: string) =>
+  const renderComponent = (pathname: string, routePath: string) =>
     render(
-      <Route path="/ui/clusters/:clusterName">
+      <WithRoute path={getNonExactPath(routePath)}>
         <Connect />
-      </Route>,
-      { pathname, store }
+      </WithRoute>,
+      { initialEntries: [pathname], store }
     );
 
   it('renders ListContainer', () => {
-    renderComponent(clusterConnectorsPath('my-cluster'));
-    expect(screen.getByText('ListContainer')).toBeInTheDocument();
+    renderComponent(
+      clusterConnectorsPath('my-cluster'),
+      clusterConnectorsPath()
+    );
+    expect(screen.getByText(ConnectCompText.list)).toBeInTheDocument();
   });
 
   it('renders NewContainer', () => {
-    renderComponent(clusterConnectorNewPath('my-cluster'));
-    expect(screen.getByText('NewContainer')).toBeInTheDocument();
+    renderComponent(
+      clusterConnectorNewPath('my-cluster'),
+      clusterConnectorsPath()
+    );
+    expect(screen.getByText(ConnectCompText.new)).toBeInTheDocument();
   });
 
   it('renders DetailsContainer', () => {
     renderComponent(
-      clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector')
+      clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector'),
+      clusterConnectsPath()
     );
-    expect(screen.getByText('DetailsContainer')).toBeInTheDocument();
+    expect(screen.getByText(ConnectCompText.details)).toBeInTheDocument();
   });
 
   it('renders EditContainer', () => {
@@ -56,8 +71,9 @@ describe('Connect', () => {
         'my-cluster',
         'my-connect',
         'my-connector'
-      )
+      ),
+      clusterConnectsPath()
     );
-    expect(screen.getByText('EditContainer')).toBeInTheDocument();
+    expect(screen.getByText(ConnectCompText.edit)).toBeInTheDocument();
   });
 });

+ 28 - 14
kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx

@@ -1,28 +1,42 @@
 import React from 'react';
-import { Switch } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
 import Details from 'components/ConsumerGroups/Details/Details';
 import ListContainer from 'components/ConsumerGroups/List/ListContainer';
 import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
 import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
+import {
+  clusterConsumerGroupResetOffsetsRelativePath,
+  RouteParams,
+} from 'lib/paths';
 
 const ConsumerGroups: React.FC = () => {
   return (
-    <Switch>
-      <BreadcrumbRoute
-        exact
-        path="/ui/clusters/:clusterName/consumer-groups"
-        component={ListContainer}
+    <Routes>
+      <Route
+        index
+        element={
+          <BreadcrumbRoute>
+            <ListContainer />
+          </BreadcrumbRoute>
+        }
       />
-      <BreadcrumbRoute
-        exact
-        path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID"
-        component={Details}
+      <Route
+        path={RouteParams.consumerGroupID}
+        element={
+          <BreadcrumbRoute>
+            <Details />
+          </BreadcrumbRoute>
+        }
       />
-      <BreadcrumbRoute
-        path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID/reset-offsets"
-        component={ResetOffsets}
+      <Route
+        path={clusterConsumerGroupResetOffsetsRelativePath}
+        element={
+          <BreadcrumbRoute>
+            <ResetOffsets />
+          </BreadcrumbRoute>
+        }
       />
-    </Switch>
+    </Routes>
   );
 };
 

+ 9 - 13
kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

@@ -1,13 +1,12 @@
 import React from 'react';
-import { ClusterName } from 'redux/interfaces';
+import { useNavigate } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import {
-  clusterConsumerGroupResetOffsetsPath,
-  clusterConsumerGroupsPath,
+  clusterConsumerGroupResetRelativePath,
+  ClusterGroupParam,
 } from 'lib/paths';
-import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
-import { useHistory, useParams } from 'react-router-dom';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
@@ -31,10 +30,9 @@ import getTagColor from 'components/common/Tag/getTagColor';
 import ListItem from './ListItem';
 
 const Details: React.FC = () => {
-  const history = useHistory();
+  const navigate = useNavigate();
   const { isReadOnly } = React.useContext(ClusterContext);
-  const { consumerGroupID, clusterName } =
-    useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
+  const { consumerGroupID, clusterName } = useAppParams<ClusterGroupParam>();
   const dispatch = useAppDispatch();
   const consumerGroup = useAppSelector((state) =>
     selectById(state, consumerGroupID)
@@ -55,14 +53,12 @@ const Details: React.FC = () => {
   };
   React.useEffect(() => {
     if (isDeleted) {
-      history.push(clusterConsumerGroupsPath(clusterName));
+      navigate('../');
     }
-  }, [clusterName, history, isDeleted]);
+  }, [clusterName, navigate, isDeleted]);
 
   const onResetOffsets = () => {
-    history.push(
-      clusterConsumerGroupResetOffsetsPath(clusterName, consumerGroupID)
-    );
+    navigate(clusterConsumerGroupResetRelativePath);
   };
 
   if (!isFetched || !consumerGroup) {

+ 8 - 11
kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx

@@ -1,13 +1,13 @@
-import { ConsumerGroupOffsetsResetType } from 'generated-sources';
-import { clusterConsumerGroupDetailsPath } from 'lib/paths';
 import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import { ConsumerGroupOffsetsResetType } from 'generated-sources';
+import { ClusterGroupParam } from 'lib/paths';
 import {
   Controller,
   FormProvider,
   useFieldArray,
   useForm,
 } from 'react-hook-form';
-import { ClusterName, ConsumerGroupID } from 'redux/interfaces';
 import MultiSelect from 'react-multi-select-component';
 import { Option } from 'react-multi-select-component/dist/lib/interfaces';
 import DatePicker from 'react-datepicker';
@@ -15,7 +15,6 @@ import 'react-datepicker/dist/react-datepicker.css';
 import { groupBy } from 'lodash';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { ErrorMessage } from '@hookform/error-message';
-import { useHistory, useParams } from 'react-router-dom';
 import Select from 'components/common/Select/Select';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import { Button } from 'components/common/Button/Button';
@@ -30,6 +29,7 @@ import {
   resetConsumerGroupOffsets,
 } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import useAppParams from 'lib/hooks/useAppParams';
 import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
 
 import {
@@ -48,8 +48,7 @@ interface FormType {
 
 const ResetOffsets: React.FC = () => {
   const dispatch = useAppDispatch();
-  const { consumerGroupID, clusterName } =
-    useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
+  const { consumerGroupID, clusterName } = useAppParams<ClusterGroupParam>();
   const consumerGroup = useAppSelector((state) =>
     selectById(state, consumerGroupID)
   );
@@ -162,15 +161,13 @@ const ResetOffsets: React.FC = () => {
     }
   };
 
-  const history = useHistory();
+  const navigate = useNavigate();
   React.useEffect(() => {
     if (isOffsetReseted) {
       dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets'));
-      history.push(
-        clusterConsumerGroupDetailsPath(clusterName, consumerGroupID)
-      );
+      navigate('../');
     }
-  }, [clusterName, consumerGroupID, dispatch, history, isOffsetReseted]);
+  }, [clusterName, consumerGroupID, dispatch, navigate, isOffsetReseted]);
 
   if (!isFetched || !consumerGroup) {
     return <PageLoader />;

+ 9 - 13
kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx

@@ -1,9 +1,8 @@
 import React from 'react';
 import fetchMock from 'fetch-mock';
-import { Route } from 'react-router-dom';
 import { act, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConsumerGroupResetOffsetsPath } from 'lib/paths';
 import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
 import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
@@ -13,19 +12,16 @@ const { groupId } = consumerGroupPayload;
 
 const renderComponent = () =>
   render(
-    <Route
-      path={clusterConsumerGroupResetOffsetsPath(
-        ':clusterName',
-        ':consumerGroupID'
-      )}
-    >
+    <WithRoute path={clusterConsumerGroupResetOffsetsPath()}>
       <ResetOffsets />
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterConsumerGroupResetOffsetsPath(
-        clusterName,
-        consumerGroupPayload.groupId
-      ),
+      initialEntries: [
+        clusterConsumerGroupResetOffsetsPath(
+          clusterName,
+          consumerGroupPayload.groupId
+        ),
+      ],
     }
   );
 

+ 9 - 10
kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/__test__/TopicContents.spec.tsx

@@ -3,28 +3,27 @@ import { clusterConsumerGroupDetailsPath } from 'lib/paths';
 import { screen } from '@testing-library/react';
 import TopicContents from 'components/ConsumerGroups/Details/TopicContents/TopicContents';
 import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
-import { render } from 'lib/testHelpers';
-import { Route } from 'react-router-dom';
+import { render, WithRoute } from 'lib/testHelpers';
 import { ConsumerGroupTopicPartition } from 'generated-sources';
 
 const clusterName = 'cluster1';
 
 const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) =>
   render(
-    <Route
-      path={clusterConsumerGroupDetailsPath(':clusterName', ':consumerGroupID')}
-    >
+    <WithRoute path={clusterConsumerGroupDetailsPath()}>
       <table>
         <tbody>
           <TopicContents consumers={consumers} />
         </tbody>
       </table>
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterConsumerGroupDetailsPath(
-        clusterName,
-        consumerGroupPayload.groupId
-      ),
+      initialEntries: [
+        clusterConsumerGroupDetailsPath(
+          clusterName,
+          consumerGroupPayload.groupId
+        ),
+      ],
     }
   );
 

+ 16 - 22
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx

@@ -1,13 +1,10 @@
 import Details from 'components/ConsumerGroups/Details/Details';
 import React from 'react';
 import fetchMock from 'fetch-mock';
-import { createMemoryHistory } from 'history';
-import { render } from 'lib/testHelpers';
-import { Route, Router } from 'react-router-dom';
+import { render, WithRoute } from 'lib/testHelpers';
 import {
   clusterConsumerGroupDetailsPath,
-  clusterConsumerGroupResetOffsetsPath,
-  clusterConsumerGroupsPath,
+  clusterConsumerGroupResetRelativePath,
 } from 'lib/paths';
 import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
 import {
@@ -20,26 +17,25 @@ import { act } from '@testing-library/react';
 
 const clusterName = 'cluster1';
 const { groupId } = consumerGroupPayload;
-const history = createMemoryHistory();
+
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+}));
 
 const renderComponent = () => {
-  history.push(clusterConsumerGroupDetailsPath(clusterName, groupId));
   render(
-    <Router history={history}>
-      <Route
-        path={clusterConsumerGroupDetailsPath(
-          ':clusterName',
-          ':consumerGroupID'
-        )}
-      >
-        <Details />
-      </Route>
-    </Router>
+    <WithRoute path={clusterConsumerGroupDetailsPath()}>
+      <Details />
+    </WithRoute>,
+    { initialEntries: [clusterConsumerGroupDetailsPath(clusterName, groupId)] }
   );
 };
 describe('Details component', () => {
   afterEach(() => {
     fetchMock.reset();
+    mockNavigate.mockClear();
   });
 
   describe('when consumer groups are NOT fetched', () => {
@@ -76,8 +72,8 @@ describe('Details component', () => {
 
     it('handles [Reset offset] click', async () => {
       userEvent.click(screen.getByText('Reset offset'));
-      expect(history.location.pathname).toEqual(
-        clusterConsumerGroupResetOffsetsPath(clusterName, groupId)
+      expect(mockNavigate).toHaveBeenLastCalledWith(
+        clusterConsumerGroupResetRelativePath
       );
     });
 
@@ -106,9 +102,7 @@ describe('Details component', () => {
       });
       expect(deleteConsumerGroupMock.called()).toBeTruthy();
       expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
-      expect(history.location.pathname).toEqual(
-        clusterConsumerGroupsPath(clusterName)
-      );
+      expect(mockNavigate).toHaveBeenLastCalledWith('../');
     });
   });
 });

+ 9 - 10
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/ListItem.spec.tsx

@@ -4,17 +4,14 @@ import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import ListItem from 'components/ConsumerGroups/Details/ListItem';
 import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
-import { render } from 'lib/testHelpers';
-import { Route } from 'react-router-dom';
+import { render, WithRoute } from 'lib/testHelpers';
 import { ConsumerGroupTopicPartition } from 'generated-sources';
 
 const clusterName = 'cluster1';
 
 const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) =>
   render(
-    <Route
-      path={clusterConsumerGroupDetailsPath(':clusterName', ':consumerGroupID')}
-    >
+    <WithRoute path={clusterConsumerGroupDetailsPath()}>
       <table>
         <tbody>
           <ListItem
@@ -24,12 +21,14 @@ const renderComponent = (consumers: ConsumerGroupTopicPartition[] = []) =>
           />
         </tbody>
       </table>
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterConsumerGroupDetailsPath(
-        clusterName,
-        consumerGroupPayload.groupId
-      ),
+      initialEntries: [
+        clusterConsumerGroupDetailsPath(
+          clusterName,
+          consumerGroupPayload.groupId
+        ),
+      ],
     }
   );
 

+ 1 - 1
kafka-ui-react-app/src/components/ConsumerGroups/List/ConsumerGroupsTableCells.tsx

@@ -17,7 +17,7 @@ export const GroupIDCell: React.FC<TableCellProps<ConsumerGroup, string>> = ({
 }) => {
   return (
     <SmartTableKeyLink>
-      <Link to={`consumer-groups/${groupId}`}>{groupId}</Link>
+      <Link to={groupId}>{groupId}</Link>
     </SmartTableKeyLink>
   );
 };

+ 3 - 3
kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import { useParams } from 'react-router-dom';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import Search from 'components/common/Search/Search';
 import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
@@ -18,7 +17,8 @@ import {
 import usePagination from 'lib/hooks/usePagination';
 import useSearch from 'lib/hooks/useSearch';
 import { useAppDispatch } from 'lib/hooks/redux';
-import { ClusterName } from 'redux/interfaces';
+import useAppParams from 'lib/hooks/useAppParams';
+import { ClusterNameRoute } from 'lib/paths';
 import { fetchConsumerGroupsPaged } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 
@@ -42,7 +42,7 @@ const List: React.FC<Props> = ({
   const { page, perPage } = usePagination();
   const [searchText, handleSearchText] = useSearch();
   const dispatch = useAppDispatch();
-  const { clusterName } = useParams<{ clusterName: ClusterName }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
 
   React.useEffect(() => {
     dispatch(

+ 1 - 4
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ConsumerGroupsTableCells.spec.tsx

@@ -40,10 +40,7 @@ describe('Consumer Groups Table Cells', () => {
       );
       const linkElement = screen.getByRole('link');
       expect(linkElement).toBeInTheDocument();
-      expect(linkElement).toHaveAttribute(
-        'href',
-        `/consumer-groups/${consumerGroup.groupId}`
-      );
+      expect(linkElement).toHaveAttribute('href', `/${consumerGroup.groupId}`);
     });
   });
 

+ 10 - 21
kafka-ui-react-app/src/components/ConsumerGroups/__test__/ConsumerGroups.spec.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { clusterConsumerGroupsPath } from 'lib/paths';
+import { clusterConsumerGroupsPath, getNonExactPath } from 'lib/paths';
 import {
   act,
   screen,
@@ -11,27 +11,19 @@ import {
   consumerGroups,
   noConsumerGroupsResponse,
 } from 'redux/reducers/consumerGroups/__test__/fixtures';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import fetchMock from 'fetch-mock';
-import { Route, Router } from 'react-router-dom';
 import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
-import { createMemoryHistory } from 'history';
 
 const clusterName = 'cluster1';
 
-const historyMock = createMemoryHistory({
-  initialEntries: [clusterConsumerGroupsPath(clusterName)],
-});
-
-const renderComponent = (history = historyMock) =>
+const renderComponent = (path?: string) =>
   render(
-    <Router history={history}>
-      <Route path={clusterConsumerGroupsPath(':clusterName')}>
-        <ConsumerGroups />
-      </Route>
-    </Router>,
+    <WithRoute path={getNonExactPath(clusterConsumerGroupsPath())}>
+      <ConsumerGroups />
+    </WithRoute>,
     {
-      pathname: clusterConsumerGroupsPath(clusterName),
+      initialEntries: [path || clusterConsumerGroupsPath(clusterName)],
     }
   );
 
@@ -123,12 +115,9 @@ describe('ConsumerGroups', () => {
         }
       );
 
-      const mockedHistory = createMemoryHistory({
-        initialEntries: [
-          `${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`,
-        ],
-      });
-      renderComponent(mockedHistory);
+      renderComponent(
+        `${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`
+      );
 
       await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
       await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy());

+ 19 - 9
kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx

@@ -1,20 +1,30 @@
 import React from 'react';
-import { Switch } from 'react-router-dom';
-import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
+import { Route, Routes } from 'react-router-dom';
+import { clusterKsqlDbQueryRelativePath } from 'lib/paths';
 import List from 'components/KsqlDb/List/List';
 import Query from 'components/KsqlDb/Query/Query';
 import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 
 const KsqlDb: React.FC = () => {
   return (
-    <Switch>
-      <BreadcrumbRoute exact path={clusterKsqlDbPath()} component={List} />
-      <BreadcrumbRoute
-        exact
-        path={clusterKsqlDbQueryPath()}
-        component={Query}
+    <Routes>
+      <Route
+        index
+        element={
+          <BreadcrumbRoute>
+            <List />
+          </BreadcrumbRoute>
+        }
       />
-    </Switch>
+      <Route
+        path={clusterKsqlDbQueryRelativePath}
+        element={
+          <BreadcrumbRoute>
+            <Query />
+          </BreadcrumbRoute>
+        }
+      />
+    </Routes>
   );
 };
 

+ 5 - 6
kafka-ui-react-app/src/components/KsqlDb/List/List.tsx

@@ -1,12 +1,12 @@
+import React, { FC, useEffect } from 'react';
+import useAppParams from 'lib/hooks/useAppParams';
 import * as Metrics from 'components/common/Metrics';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import ListItem from 'components/KsqlDb/List/ListItem';
-import React, { FC, useEffect } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
-import { useParams } from 'react-router-dom';
 import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
 import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
-import { clusterKsqlDbQueryPath } from 'lib/paths';
+import { clusterKsqlDbQueryRelativePath, ClusterNameRoute } from 'lib/paths';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
@@ -25,7 +25,7 @@ const accessors = headers.map((header) => header.accessor);
 const List: FC = () => {
   const dispatch = useDispatch();
 
-  const { clusterName } = useParams<{ clusterName: string }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
 
   const { rows, fetching, tablesCount, streamsCount } =
     useSelector(getKsqlDbTables);
@@ -38,8 +38,7 @@ const List: FC = () => {
     <>
       <PageHeading text="KSQL DB">
         <Button
-          isLink
-          to={clusterKsqlDbQueryPath(clusterName)}
+          to={clusterKsqlDbQueryRelativePath}
           buttonType="primary"
           buttonSize="M"
         >

+ 5 - 10
kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx

@@ -1,23 +1,18 @@
 import React from 'react';
 import List from 'components/KsqlDb/List/List';
-import { Route, Router } from 'react-router-dom';
-import { createMemoryHistory } from 'history';
 import { clusterKsqlDbPath } from 'lib/paths';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import fetchMock from 'fetch-mock';
 import { screen, waitForElementToBeRemoved } from '@testing-library/dom';
 
-const history = createMemoryHistory();
 const clusterName = 'local';
 
 const renderComponent = () => {
-  history.push(clusterKsqlDbPath(clusterName));
   render(
-    <Router history={history}>
-      <Route path={clusterKsqlDbPath(':clusterName')}>
-        <List />
-      </Route>
-    </Router>
+    <WithRoute path={clusterKsqlDbPath()}>
+      <List />
+    </WithRoute>,
+    { initialEntries: [clusterKsqlDbPath(clusterName)] }
   );
 };
 

+ 5 - 10
kafka-ui-react-app/src/components/KsqlDb/List/__test__/ListItem.spec.tsx

@@ -1,12 +1,9 @@
 import React from 'react';
-import { Route, Router } from 'react-router-dom';
-import { createMemoryHistory } from 'history';
 import { clusterKsqlDbPath } from 'lib/paths';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { screen } from '@testing-library/dom';
 import ListItem from 'components/KsqlDb/List/ListItem';
 
-const history = createMemoryHistory();
 const clusterName = 'local';
 
 const renderComponent = ({
@@ -16,13 +13,11 @@ const renderComponent = ({
   accessors: string[];
   data: Record<string, string>;
 }) => {
-  history.push(clusterKsqlDbPath(clusterName));
   render(
-    <Router history={history}>
-      <Route path={clusterKsqlDbPath(':clusterName')}>
-        <ListItem accessors={accessors} data={data} />
-      </Route>
-    </Router>
+    <WithRoute path={clusterKsqlDbPath()}>
+      <ListItem accessors={accessors} data={data} />
+    </WithRoute>,
+    { initialEntries: [clusterKsqlDbPath(clusterName)] }
   );
 };
 

+ 3 - 2
kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx

@@ -1,5 +1,5 @@
 import React, { useCallback, useEffect, FC, useState } from 'react';
-import { useParams } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import TableRenderer from 'components/KsqlDb/Query/renderer/TableRenderer/TableRenderer';
 import {
   executeKsql,
@@ -11,6 +11,7 @@ import { BASE_PARAMS } from 'lib/constants';
 import { KsqlResponse, KsqlTableResponse } from 'generated-sources';
 import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice';
 import { now } from 'lodash';
+import { ClusterNameRoute } from 'lib/paths';
 
 import type { FormValues } from './QueryForm/QueryForm';
 import * as S from './Query.styled';
@@ -61,7 +62,7 @@ export const getFormattedErrorFromTableData = (
 };
 
 const Query: FC = () => {
-  const { clusterName } = useParams<{ clusterName: string }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
 
   const sseRef = React.useRef<{ sse: EventSource | null; isOpen: boolean }>({
     sse: null,

+ 4 - 5
kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx

@@ -1,4 +1,4 @@
-import { render, EventSourceMock } from 'lib/testHelpers';
+import { render, EventSourceMock, WithRoute } from 'lib/testHelpers';
 import React from 'react';
 import Query, {
   getFormattedErrorFromTableData,
@@ -6,18 +6,17 @@ import Query, {
 import { screen, within } from '@testing-library/dom';
 import fetchMock from 'fetch-mock';
 import userEvent from '@testing-library/user-event';
-import { Route } from 'react-router-dom';
 import { clusterKsqlDbQueryPath } from 'lib/paths';
 import { act } from '@testing-library/react';
 
 const clusterName = 'testLocal';
 const renderComponent = () =>
   render(
-    <Route path={clusterKsqlDbQueryPath(':clusterName')}>
+    <WithRoute path={clusterKsqlDbQueryPath()}>
       <Query />
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterKsqlDbQueryPath(clusterName),
+      initialEntries: [clusterKsqlDbQueryPath(clusterName)],
     }
   );
 

+ 35 - 8
kafka-ui-react-app/src/components/KsqlDb/__test__/KsqlDb.spec.tsx

@@ -1,15 +1,42 @@
 import React from 'react';
 import KsqlDb from 'components/KsqlDb/KsqlDb';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { screen } from '@testing-library/dom';
-import { clusterKsqlDbPath } from 'lib/paths';
+import {
+  clusterKsqlDbPath,
+  clusterKsqlDbQueryPath,
+  getNonExactPath,
+} from 'lib/paths';
+
+const KSqLComponentText = {
+  list: 'list',
+  query: 'query',
+};
+
+jest.mock('components/KsqlDb/List/List', () => () => (
+  <div>{KSqLComponentText.list}</div>
+));
+jest.mock('components/KsqlDb/Query/Query', () => () => (
+  <div>{KSqLComponentText.query}</div>
+));
 
 describe('KsqlDb Component', () => {
-  describe('KsqlDb', () => {
-    it('to be in the document', () => {
-      render(<KsqlDb />, { pathname: clusterKsqlDbPath() });
-      expect(screen.getByText('KSQL DB')).toBeInTheDocument();
-      expect(screen.getByText('Execute KSQL Request')).toBeInTheDocument();
-    });
+  const clusterName = 'clusterName';
+  const renderComponent = (path: string) =>
+    render(
+      <WithRoute path={getNonExactPath(clusterKsqlDbPath())}>
+        <KsqlDb />
+      </WithRoute>,
+      { initialEntries: [path] }
+    );
+
+  it('Renders the List', () => {
+    renderComponent(clusterKsqlDbPath(clusterName));
+    expect(screen.getByText(KSqLComponentText.list)).toBeInTheDocument();
+  });
+
+  it('Renders the List', () => {
+    renderComponent(clusterKsqlDbQueryPath(clusterName));
+    expect(screen.getByText(KSqLComponentText.query)).toBeInTheDocument();
   });
 });

+ 0 - 5
kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx

@@ -6,7 +6,6 @@ import {
   clusterConsumerGroupsPath,
   clusterSchemasPath,
   clusterConnectorsPath,
-  clusterConnectsPath,
   clusterKsqlDbPath,
 } from 'lib/paths';
 
@@ -54,10 +53,6 @@ const ClusterMenu: React.FC<Props> = ({
             <ClusterMenuItem
               to={clusterConnectorsPath(name)}
               title="Kafka Connect"
-              isActive={(_, location) =>
-                location.pathname.startsWith(clusterConnectsPath(name)) ||
-                location.pathname.startsWith(clusterConnectorsPath(name))
-              }
             />
           )}
           {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && (

+ 2 - 5
kafka-ui-react-app/src/components/Nav/ClusterMenuItem.tsx

@@ -1,25 +1,22 @@
 import React, { PropsWithChildren } from 'react';
-import { NavLinkProps } from 'react-router-dom';
 
 import * as S from './Nav.styled';
 
 export interface ClusterMenuItemProps {
   to: string;
   title?: string;
-  exact?: boolean;
   isTopLevel?: boolean;
-  isActive?: NavLinkProps['isActive'];
 }
 
 const ClusterMenuItem: React.FC<PropsWithChildren<ClusterMenuItemProps>> = (
   props
 ) => {
-  const { to, title, children, exact, isTopLevel, isActive } = props;
+  const { to, title, children, isTopLevel } = props;
 
   if (to) {
     return (
       <S.ListItem $isTopLevel={isTopLevel}>
-        <S.Link to={to} title={title} exact={exact} isActive={isActive}>
+        <S.Link to={to} title={title}>
           {title}
         </S.Link>
         {children}

+ 4 - 5
kafka-ui-react-app/src/components/Nav/Nav.styled.ts

@@ -14,13 +14,13 @@ export const Divider = styled.hr`
   height: 1px;
 `;
 
-export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })(
-  ({ theme, activeClassName }) => css`
+export const Link = styled(NavLink)(
+  ({ theme }) => css`
     width: 100%;
     padding: 0.5em 0.75em;
     cursor: pointer;
     text-decoration: none;
-    margin: 0px 0px;
+    margin: 0 0;
     background-color: ${theme.menu.backgroundColor.normal};
     color: ${theme.menu.color.normal};
 
@@ -28,8 +28,7 @@ export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })(
       background-color: ${theme.menu.backgroundColor.hover};
       color: ${theme.menu.color.hover};
     }
-
-    &.${activeClassName} {
+    &.active {
       background-color: ${theme.menu.backgroundColor.active};
       color: ${theme.menu.color.active};
     }

+ 1 - 1
kafka-ui-react-app/src/components/Nav/Nav.tsx

@@ -13,7 +13,7 @@ interface Props {
 const Nav: React.FC<Props> = ({ areClustersFulfilled, clusters }) => (
   <aside aria-label="Sidebar Menu">
     <S.List>
-      <ClusterMenuItem exact to="/" title="Dashboard" isTopLevel />
+      <ClusterMenuItem to="/" title="Dashboard" isTopLevel />
     </S.List>
 
     {areClustersFulfilled &&

+ 6 - 19
kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx

@@ -4,7 +4,7 @@ import { Cluster, ClusterFeaturesEnum } from 'generated-sources';
 import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
 import ClusterMenu from 'components/Nav/ClusterMenu';
 import userEvent from '@testing-library/user-event';
-import { clusterConnectorsPath, clusterConnectsPath } from 'lib/paths';
+import { clusterConnectorsPath } from 'lib/paths';
 import { render } from 'lib/testHelpers';
 
 describe('ClusterMenu', () => {
@@ -55,7 +55,7 @@ describe('ClusterMenu', () => {
   });
   it('renders open cluster menu', () => {
     render(setupComponent(onlineClusterPayload, true), {
-      pathname: clusterConnectorsPath(onlineClusterPayload.name),
+      initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)],
     });
 
     expect(getMenuItems().length).toEqual(4);
@@ -70,28 +70,15 @@ describe('ClusterMenu', () => {
         ...onlineClusterPayload,
         features: [ClusterFeaturesEnum.KAFKA_CONNECT],
       }),
-      { pathname: clusterConnectorsPath(onlineClusterPayload.name) }
+      { initialEntries: [clusterConnectorsPath(onlineClusterPayload.name)] }
     );
     expect(getMenuItems().length).toEqual(1);
     userEvent.click(getMenuItem());
     expect(getMenuItems().length).toEqual(5);
 
-    expect(getKafkaConnect()).toBeInTheDocument();
-    expect(getKafkaConnect()).toHaveClass('is-active');
-  });
-  it('makes Kafka Connect link active', () => {
-    render(
-      setupComponent({
-        ...onlineClusterPayload,
-        features: [ClusterFeaturesEnum.KAFKA_CONNECT],
-      }),
-      { pathname: clusterConnectsPath(onlineClusterPayload.name) }
-    );
-    expect(getMenuItems().length).toEqual(1);
-    userEvent.click(getMenuItem());
-    expect(getMenuItems().length).toEqual(5);
+    const kafkaConnect = getKafkaConnect();
+    expect(kafkaConnect).toBeInTheDocument();
 
-    expect(getKafkaConnect()).toBeInTheDocument();
-    expect(getKafkaConnect()).toHaveClass('is-active');
+    expect(getKafkaConnect()).toHaveClass('active');
   });
 });

+ 10 - 12
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
 import {
-  clusterSchemasPath,
-  clusterSchemaSchemaDiffPath,
-  clusterSchemaEditPath,
+  ClusterSubjectParam,
+  clusterSchemaEditPageRelativePath,
+  clusterSchemaSchemaDiffRelativePath,
 } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
@@ -31,16 +31,16 @@ import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
 import { getResponse } from 'lib/errorHandling';
 import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
 import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import LatestVersionItem from './LatestVersion/LatestVersionItem';
 import SchemaVersion from './SchemaVersion/SchemaVersion';
 
 const Details: React.FC = () => {
-  const history = useHistory();
+  const navigate = useNavigate();
   const dispatch = useAppDispatch();
   const { isReadOnly } = React.useContext(ClusterContext);
-  const { clusterName, subject } =
-    useParams<{ clusterName: string; subject: string }>();
+  const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
   const [
     isDeleteSchemaConfirmationVisible,
     setDeleteSchemaConfirmationVisible,
@@ -71,7 +71,7 @@ const Details: React.FC = () => {
         clusterName,
         subject,
       });
-      history.push(clusterSchemasPath(clusterName));
+      navigate('../');
     } catch (e) {
       const err = await getResponse(e as Response);
       dispatch(serverErrorAlertAdded(err));
@@ -87,21 +87,19 @@ const Details: React.FC = () => {
         {!isReadOnly && (
           <>
             <Button
-              isLink
               buttonSize="M"
               buttonType="primary"
               to={{
-                pathname: clusterSchemaSchemaDiffPath(clusterName, subject),
+                pathname: clusterSchemaSchemaDiffRelativePath,
                 search: `leftVersion=${versions[0]?.version}&rightVersion=${versions[0]?.version}`,
               }}
             >
               Compare Versions
             </Button>
             <Button
-              isLink
               buttonSize="M"
               buttonType="primary"
-              to={clusterSchemaEditPath(clusterName, subject)}
+              to={clusterSchemaEditPageRelativePath}
             >
               Edit Schema
             </Button>

+ 4 - 5
kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 import Details from 'components/Schemas/Details/Details';
-import { render } from 'lib/testHelpers';
-import { Route } from 'react-router-dom';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterSchemaPath } from 'lib/paths';
 import { screen, waitFor } from '@testing-library/dom';
 import {
@@ -27,13 +26,13 @@ const renderComponent = (
   context: ContextProps = contextInitialValue
 ) =>
   render(
-    <Route path={clusterSchemaPath(':clusterName', ':subject')}>
+    <WithRoute path={clusterSchemaPath()}>
       <ClusterContext.Provider value={context}>
         <Details />
       </ClusterContext.Provider>
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterSchemaPath(clusterName, schemaVersion.subject),
+      initialEntries: [clusterSchemaPath(clusterName, schemaVersion.subject)],
       preloadedState: {
         schemas: initialState,
       },

+ 21 - 25
kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
 import { SchemaSubject } from 'generated-sources';
-import { clusterSchemaSchemaDiffPath } from 'lib/paths';
+import { clusterSchemaSchemaDiffPath, ClusterSubjectParam } from 'lib/paths';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import DiffViewer from 'components/common/DiffViewer/DiffViewer';
-import { useHistory, useParams, useLocation } from 'react-router-dom';
+import { useNavigate, useLocation } from 'react-router-dom';
 import {
   fetchSchemaVersions,
   SCHEMAS_VERSIONS_FETCH_ACTION,
@@ -12,31 +12,32 @@ import { useForm, Controller } from 'react-hook-form';
 import Select from 'components/common/Select/Select';
 import { useAppDispatch } from 'lib/hooks/redux';
 import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import * as S from './Diff.styled';
 
 export interface DiffProps {
-  leftVersionInPath?: string;
-  rightVersionInPath?: string;
   versions: SchemaSubject[];
   areVersionsFetched: boolean;
 }
 
-const Diff: React.FC<DiffProps> = ({
-  leftVersionInPath,
-  rightVersionInPath,
-  versions,
-  areVersionsFetched,
-}) => {
-  const [leftVersion, setLeftVersion] = React.useState(leftVersionInPath || '');
+const Diff: React.FC<DiffProps> = ({ versions, areVersionsFetched }) => {
+  const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
+  const navigate = useNavigate();
+  const location = useLocation();
+
+  const searchParams = React.useMemo(
+    () => new URLSearchParams(location.search),
+    [location]
+  );
+
+  const [leftVersion, setLeftVersion] = React.useState(
+    searchParams.get('leftVersion') || ''
+  );
   const [rightVersion, setRightVersion] = React.useState(
-    rightVersionInPath || ''
+    searchParams.get('rightVersion') || ''
   );
-  const history = useHistory();
-  const location = useLocation();
 
-  const { clusterName, subject } =
-    useParams<{ clusterName: string; subject: string }>();
   const dispatch = useAppDispatch();
 
   React.useEffect(() => {
@@ -64,11 +65,6 @@ const Diff: React.FC<DiffProps> = ({
     control,
   } = methods;
 
-  const searchParams = React.useMemo(
-    () => new URLSearchParams(location.search),
-    [location]
-  );
-
   return (
     <S.Section>
       {areVersionsFetched ? (
@@ -89,7 +85,7 @@ const Diff: React.FC<DiffProps> = ({
                         leftVersion === '' ? versions[0].version : leftVersion
                       }
                       onChange={(event) => {
-                        history.push(
+                        navigate(
                           clusterSchemaSchemaDiffPath(clusterName, subject)
                         );
                         searchParams.set('leftVersion', event.toString());
@@ -99,7 +95,7 @@ const Diff: React.FC<DiffProps> = ({
                             ? versions[0].version
                             : rightVersion
                         );
-                        history.push({
+                        navigate({
                           search: `?${searchParams.toString()}`,
                         });
                         setLeftVersion(event.toString());
@@ -130,7 +126,7 @@ const Diff: React.FC<DiffProps> = ({
                         rightVersion === '' ? versions[0].version : rightVersion
                       }
                       onChange={(event) => {
-                        history.push(
+                        navigate(
                           clusterSchemaSchemaDiffPath(clusterName, subject)
                         );
                         searchParams.set(
@@ -138,7 +134,7 @@ const Diff: React.FC<DiffProps> = ({
                           leftVersion === '' ? versions[0].version : leftVersion
                         );
                         searchParams.set('rightVersion', event.toString());
-                        history.push({
+                        navigate({
                           search: `?${searchParams.toString()}`,
                         });
                         setRightVersion(event.toString());

+ 2 - 19
kafka-ui-react-app/src/components/Schemas/Diff/DiffContainer.ts

@@ -1,6 +1,5 @@
 import { connect } from 'react-redux';
 import { RootState } from 'redux/interfaces';
-import { RouteComponentProps, withRouter } from 'react-router-dom';
 import {
   getAreSchemaVersionsFulfilled,
   selectAllSchemaVersions,
@@ -8,25 +7,9 @@ import {
 
 import Diff from './Diff';
 
-interface RouteProps {
-  leftVersion?: string;
-  rightVersion?: string;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { leftVersion, rightVersion },
-    },
-  }: OwnProps
-) => ({
+const mapStateToProps = (state: RootState) => ({
   versions: selectAllSchemaVersions(state),
   areVersionsFetched: getAreSchemaVersionsFulfilled(state),
-  leftVersionInPath: leftVersion,
-  rightVersionInPath: rightVersion,
 });
 
-export default withRouter(connect(mapStateToProps)(Diff));
+export default connect(mapStateToProps)(Diff);

+ 51 - 20
kafka-ui-react-app/src/components/Schemas/Diff/__test__/Diff.spec.tsx

@@ -1,20 +1,46 @@
 import React from 'react';
 import Diff, { DiffProps } from 'components/Schemas/Diff/Diff';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { screen } from '@testing-library/react';
+import { clusterSchemaSchemaDiffPath } from 'lib/paths';
 
 import { versions } from './fixtures';
 
+const defaultClusterName = 'defaultClusterName';
+const defaultSubject = 'defaultSubject';
+const defaultPathName = clusterSchemaSchemaDiffPath(
+  defaultClusterName,
+  defaultSubject
+);
+
 describe('Diff', () => {
-  const setupComponent = (props: DiffProps) =>
-    render(
-      <Diff
-        versions={props.versions}
-        leftVersionInPath={props.leftVersionInPath}
-        rightVersionInPath={props.rightVersionInPath}
-        areVersionsFetched={props.areVersionsFetched}
-      />
+  const setupComponent = (
+    props: DiffProps,
+    searchQuery: { rightVersion?: string; leftVersion?: string } = {}
+  ) => {
+    let pathname = defaultPathName;
+    const searchParams = new URLSearchParams(pathname);
+    if (searchQuery.rightVersion) {
+      searchParams.set('rightVersion', searchQuery.rightVersion);
+    }
+    if (searchQuery.leftVersion) {
+      searchParams.set('leftVersion', searchQuery.leftVersion);
+    }
+
+    pathname = `${pathname}?${searchParams.toString()}`;
+
+    return render(
+      <WithRoute path={clusterSchemaSchemaDiffPath()}>
+        <Diff
+          versions={props.versions}
+          areVersionsFetched={props.areVersionsFetched}
+        />
+      </WithRoute>,
+      {
+        initialEntries: [pathname],
+      }
     );
+  };
 
   describe('Container', () => {
     it('renders view', () => {
@@ -69,12 +95,13 @@ describe('Diff', () => {
   });
   describe('when schema versions are loaded and two versions in path', () => {
     beforeEach(() => {
-      setupComponent({
-        areVersionsFetched: true,
-        versions,
-        leftVersionInPath: '1',
-        rightVersionInPath: '2',
-      });
+      setupComponent(
+        {
+          areVersionsFetched: true,
+          versions,
+        },
+        { leftVersion: '1', rightVersion: '2' }
+      );
     });
 
     it('renders left select with version 1', () => {
@@ -92,11 +119,15 @@ describe('Diff', () => {
 
   describe('when schema versions are loaded and only one versions in path', () => {
     beforeEach(() => {
-      setupComponent({
-        areVersionsFetched: true,
-        versions,
-        leftVersionInPath: '1',
-      });
+      setupComponent(
+        {
+          areVersionsFetched: true,
+          versions,
+        },
+        {
+          leftVersion: '1',
+        }
+      );
     });
 
     it('renders left select with version 1', () => {

+ 6 - 6
kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
 import { useForm, Controller, FormProvider } from 'react-hook-form';
 import {
   CompatibilityLevelCompatibilityEnum,
   SchemaType,
 } from 'generated-sources';
-import { clusterSchemaPath } from 'lib/paths';
+import { clusterSchemaPath, ClusterSubjectParam } from 'lib/paths';
 import { NewSchemaSubjectRaw } from 'redux/interfaces';
 import Editor from 'components/common/Editor/Editor';
 import Select from 'components/common/Select/Select';
@@ -13,6 +13,7 @@ import { Button } from 'components/common/Button/Button';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import useAppParams from 'lib/hooks/useAppParams';
 import {
   schemaAdded,
   schemasApiClient,
@@ -30,11 +31,10 @@ import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
 import * as S from './Edit.styled';
 
 const Edit: React.FC = () => {
-  const history = useHistory();
+  const navigate = useNavigate();
   const dispatch = useAppDispatch();
 
-  const { clusterName, subject } =
-    useParams<{ clusterName: string; subject: string }>();
+  const { clusterName, subject } = useAppParams<ClusterSubjectParam>();
   const methods = useForm<NewSchemaSubjectRaw>({ mode: 'onChange' });
   const {
     formState: { isDirty, isSubmitting, dirtyFields },
@@ -90,7 +90,7 @@ const Edit: React.FC = () => {
         );
       }
 
-      history.push(clusterSchemaPath(clusterName, subject));
+      navigate(clusterSchemaPath(clusterName, subject));
     } catch (e) {
       const err = await getResponse(e as Response);
       dispatch(serverErrorAlertAdded(err));

+ 7 - 6
kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx

@@ -1,12 +1,11 @@
 import React from 'react';
 import Edit from 'components/Schemas/Edit/Edit';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterSchemaEditPath } from 'lib/paths';
 import {
   schemasInitialState,
   schemaVersion,
 } from 'redux/reducers/schemas/__test__/fixtures';
-import { Route } from 'react-router-dom';
 import { screen, waitFor } from '@testing-library/dom';
 import ClusterContext, {
   ContextProps,
@@ -24,13 +23,15 @@ const renderComponent = (
   context: ContextProps = contextInitialValue
 ) =>
   render(
-    <Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
+    <WithRoute path={clusterSchemaEditPath()}>
       <ClusterContext.Provider value={context}>
         <Edit />
       </ClusterContext.Provider>
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterSchemaEditPath(clusterName, schemaVersion.subject),
+      initialEntries: [
+        clusterSchemaEditPath(clusterName, schemaVersion.subject),
+      ],
       preloadedState: {
         schemas: initialState,
       },
@@ -41,7 +42,7 @@ describe('Edit', () => {
   afterEach(() => fetchMock.reset());
 
   describe('fetch failed', () => {
-    it('renders pageloader', async () => {
+    it('renders page loader', async () => {
       const schemasAPILatestMock = fetchMock.getOnce(schemasAPILatestUrl, 404);
       await act(() => {
         renderComponent();

+ 4 - 3
kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx

@@ -1,3 +1,4 @@
+import React from 'react';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import Select from 'components/common/Select/Select';
 import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
@@ -5,18 +6,18 @@ import { getResponse } from 'lib/errorHandling';
 import { useAppDispatch } from 'lib/hooks/redux';
 import usePagination from 'lib/hooks/usePagination';
 import useSearch from 'lib/hooks/useSearch';
-import React from 'react';
-import { useParams } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
 import {
   fetchSchemas,
   schemasApiClient,
 } from 'redux/reducers/schemas/schemasSlice';
+import { ClusterNameRoute } from 'lib/paths';
 
 import * as S from './GlobalSchemaSelector.styled';
 
 const GlobalSchemaSelector: React.FC = () => {
-  const { clusterName } = useParams<{ clusterName: string }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
   const dispatch = useAppDispatch();
   const [searchText] = useSearch();
   const { page, perPage } = usePagination();

+ 4 - 5
kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/__test__/GlobalSchemaSelector.spec.tsx

@@ -1,11 +1,10 @@
 import React from 'react';
 import { act, screen, waitFor, within } from '@testing-library/react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
 import GlobalSchemaSelector from 'components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector';
 import userEvent from '@testing-library/user-event';
 import { clusterSchemasPath } from 'lib/paths';
-import { Route } from 'react-router-dom';
 import fetchMock from 'fetch-mock';
 
 const clusterName = 'testClusterName';
@@ -29,11 +28,11 @@ const expectOptionIsSelected = (option: string) => {
 describe('GlobalSchemaSelector', () => {
   const renderComponent = () =>
     render(
-      <Route path={clusterSchemasPath(':clusterName')}>
+      <WithRoute path={clusterSchemasPath()}>
         <GlobalSchemaSelector />
-      </Route>,
+      </WithRoute>,
       {
-        pathname: clusterSchemasPath(clusterName),
+        initialEntries: [clusterSchemasPath(clusterName)],
       }
     );
 

+ 4 - 5
kafka-ui-react-app/src/components/Schemas/List/List.tsx

@@ -1,12 +1,12 @@
 import React from 'react';
-import { useParams } from 'react-router-dom';
-import { clusterSchemaNewPath } from 'lib/paths';
+import { ClusterNameRoute, clusterSchemaNewRelativePath } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import * as C from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import { Button } from 'components/common/Button/Button';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import useAppParams from 'lib/hooks/useAppParams';
 import {
   selectAllSchemas,
   fetchSchemas,
@@ -27,7 +27,7 @@ import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
 const List: React.FC = () => {
   const dispatch = useAppDispatch();
   const { isReadOnly } = React.useContext(ClusterContext);
-  const { clusterName } = useParams<{ clusterName: string }>();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
 
   const schemas = useAppSelector(selectAllSchemas);
   const isFetched = useAppSelector(getAreSchemasFulfilled);
@@ -52,8 +52,7 @@ const List: React.FC = () => {
             <Button
               buttonSize="M"
               buttonType="primary"
-              isLink
-              to={clusterSchemaNewPath(clusterName)}
+              to={clusterSchemaNewRelativePath}
             >
               <i className="fas fa-plus" /> Create Schema
             </Button>

+ 1 - 1
kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx

@@ -13,7 +13,7 @@ const ListItem: React.FC<ListItemProps> = ({
   return (
     <tr>
       <S.TableKeyLink>
-        <NavLink exact to={`schemas/${subject}`} role="link">
+        <NavLink to={subject} role="link">
           {subject}
         </NavLink>
       </S.TableKeyLink>

+ 4 - 5
kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 import List from 'components/Schemas/List/List';
-import { render } from 'lib/testHelpers';
-import { Route } from 'react-router-dom';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterSchemasPath } from 'lib/paths';
 import { act, screen } from '@testing-library/react';
 import {
@@ -27,13 +26,13 @@ const renderComponent = (
   context: ContextProps = contextInitialValue
 ) =>
   render(
-    <Route path={clusterSchemasPath(':clusterName')}>
+    <WithRoute path={clusterSchemasPath()}>
       <ClusterContext.Provider value={context}>
         <List />
       </ClusterContext.Provider>
-    </Route>,
+    </WithRoute>,
     {
-      pathname: clusterSchemasPath(clusterName),
+      initialEntries: [clusterSchemasPath(clusterName)],
       preloadedState: {
         schemas: initialState,
       },

+ 6 - 5
kafka-ui-react-app/src/components/Schemas/New/New.tsx

@@ -2,10 +2,10 @@ import React from 'react';
 import { NewSchemaSubjectRaw } from 'redux/interfaces';
 import { FormProvider, useForm, Controller } from 'react-hook-form';
 import { ErrorMessage } from '@hookform/error-message';
-import { clusterSchemaPath } from 'lib/paths';
+import { ClusterNameRoute, clusterSchemaPath } from 'lib/paths';
 import { SchemaType } from 'generated-sources';
 import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import Input from 'components/common/Input/Input';
 import { FormError } from 'components/common/Input/Input.styled';
@@ -18,6 +18,7 @@ import {
   schemasApiClient,
 } from 'redux/reducers/schemas/schemasSlice';
 import { useAppDispatch } from 'lib/hooks/redux';
+import useAppParams from 'lib/hooks/useAppParams';
 import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
 import { getResponse } from 'lib/errorHandling';
 
@@ -30,8 +31,8 @@ const SchemaTypeOptions: Array<SelectOption> = [
 ];
 
 const New: React.FC = () => {
-  const { clusterName } = useParams<{ clusterName: string }>();
-  const history = useHistory();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
+  const navigate = useNavigate();
   const dispatch = useAppDispatch();
   const methods = useForm<NewSchemaSubjectRaw>();
   const {
@@ -52,7 +53,7 @@ const New: React.FC = () => {
         newSchemaSubject: { subject, schema, schemaType },
       });
       dispatch(schemaAdded(resp));
-      history.push(clusterSchemaPath(clusterName, subject));
+      navigate(clusterSchemaPath(clusterName, subject));
     } catch (e) {
       const err = await getResponse(e as Response);
       dispatch(serverErrorAlertAdded(err));

+ 4 - 5
kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx

@@ -1,8 +1,7 @@
 import React from 'react';
 import New from 'components/Schemas/New/New';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { clusterSchemaNewPath } from 'lib/paths';
-import { Route } from 'react-router-dom';
 import { screen } from '@testing-library/dom';
 
 const clusterName = 'local';
@@ -10,11 +9,11 @@ const clusterName = 'local';
 describe('New Component', () => {
   beforeEach(() => {
     render(
-      <Route path={clusterSchemaNewPath(':clusterName')}>
+      <WithRoute path={clusterSchemaNewPath()}>
         <New />
-      </Route>,
+      </WithRoute>,
       {
-        pathname: clusterSchemaNewPath(clusterName),
+        initialEntries: [clusterSchemaNewPath(clusterName)],
       }
     );
   });

+ 41 - 28
kafka-ui-react-app/src/components/Schemas/Schemas.tsx

@@ -1,11 +1,9 @@
 import React from 'react';
-import { Switch } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
 import {
-  clusterSchemaNewPath,
-  clusterSchemaPath,
-  clusterSchemaEditPath,
-  clusterSchemasPath,
-  clusterSchemaSchemaDiffPath,
+  clusterSchemaEditRelativePath,
+  clusterSchemaNewRelativePath,
+  RouteParams,
 } from 'lib/paths';
 import List from 'components/Schemas/List/List';
 import Details from 'components/Schemas/Details/Details';
@@ -16,33 +14,48 @@ import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 
 const Schemas: React.FC = () => {
   return (
-    <Switch>
-      <BreadcrumbRoute
-        exact
-        path={clusterSchemasPath(':clusterName')}
-        component={List}
+    <Routes>
+      <Route
+        index
+        element={
+          <BreadcrumbRoute>
+            <List />
+          </BreadcrumbRoute>
+        }
       />
-      <BreadcrumbRoute
-        exact
-        path={clusterSchemaNewPath(':clusterName')}
-        component={New}
+      <Route
+        path={clusterSchemaNewRelativePath}
+        element={
+          <BreadcrumbRoute>
+            <New />
+          </BreadcrumbRoute>
+        }
       />
-      <BreadcrumbRoute
-        exact
-        path={clusterSchemaPath(':clusterName', ':subject')}
-        component={Details}
+      <Route
+        path={RouteParams.subject}
+        element={
+          <BreadcrumbRoute>
+            <Details />
+          </BreadcrumbRoute>
+        }
       />
-      <BreadcrumbRoute
-        exact
-        path={clusterSchemaEditPath(':clusterName', ':subject')}
-        component={Edit}
+      <Route
+        path={clusterSchemaEditRelativePath}
+        element={
+          <BreadcrumbRoute>
+            <Edit />
+          </BreadcrumbRoute>
+        }
       />
-      <BreadcrumbRoute
-        exact
-        path={clusterSchemaSchemaDiffPath(':clusterName', ':subject')}
-        component={DiffContainer}
+      <Route
+        path={clusterSchemaEditRelativePath}
+        element={
+          <BreadcrumbRoute>
+            <DiffContainer />
+          </BreadcrumbRoute>
+        }
       />
-    </Switch>
+    </Routes>
   );
 };
 

+ 34 - 14
kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx

@@ -1,32 +1,46 @@
 import React from 'react';
 import Schemas from 'components/Schemas/Schemas';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import {
-  clusterPath,
   clusterSchemaEditPath,
   clusterSchemaNewPath,
   clusterSchemaPath,
   clusterSchemasPath,
+  getNonExactPath,
 } from 'lib/paths';
 import { screen, waitFor } from '@testing-library/dom';
-import { Route } from 'react-router-dom';
 import fetchMock from 'fetch-mock';
 import { schemaVersion } from 'redux/reducers/schemas/__test__/fixtures';
 
 const renderComponent = (pathname: string) =>
   render(
-    <Route path={clusterPath(':clusterName')}>
+    <WithRoute path={getNonExactPath(clusterSchemasPath())}>
       <Schemas />
-    </Route>,
-    { pathname }
+    </WithRoute>,
+    { initialEntries: [pathname] }
   );
 
 const clusterName = 'secondLocal';
 
-jest.mock('components/Schemas/List/List', () => () => <div>List</div>);
-jest.mock('components/Schemas/Details/Details', () => () => <div>Details</div>);
-jest.mock('components/Schemas/New/New', () => () => <div>New</div>);
-jest.mock('components/Schemas/Edit/Edit', () => () => <div>Edit</div>);
+const SchemaCompText = {
+  List: 'List',
+  Details: 'Details',
+  New: 'New',
+  Edit: 'Edit',
+};
+
+jest.mock('components/Schemas/List/List', () => () => (
+  <div>{SchemaCompText.List}</div>
+));
+jest.mock('components/Schemas/Details/Details', () => () => (
+  <div>{SchemaCompText.Details}</div>
+));
+jest.mock('components/Schemas/New/New', () => () => (
+  <div>{SchemaCompText.New}</div>
+));
+jest.mock('components/Schemas/Edit/Edit', () => () => (
+  <div>{SchemaCompText.Edit}</div>
+));
 
 describe('Schemas', () => {
   beforeEach(() => {
@@ -35,20 +49,26 @@ describe('Schemas', () => {
   afterEach(() => fetchMock.restore());
   it('renders List', async () => {
     renderComponent(clusterSchemasPath(clusterName));
-    await waitFor(() => expect(screen.queryByText('List')).toBeInTheDocument());
+    await waitFor(() =>
+      expect(screen.queryByText(SchemaCompText.List)).toBeInTheDocument()
+    );
   });
   it('renders New', async () => {
     renderComponent(clusterSchemaNewPath(clusterName));
-    await waitFor(() => expect(screen.queryByText('New')).toBeInTheDocument());
+    await waitFor(() =>
+      expect(screen.queryByText(SchemaCompText.New)).toBeInTheDocument()
+    );
   });
   it('renders Details', async () => {
     renderComponent(clusterSchemaPath(clusterName, schemaVersion.subject));
     await waitFor(() =>
-      expect(screen.queryByText('Details')).toBeInTheDocument()
+      expect(screen.queryByText(SchemaCompText.Details)).toBeInTheDocument()
     );
   });
   it('renders Edit', async () => {
     renderComponent(clusterSchemaEditPath(clusterName, schemaVersion.subject));
-    await waitFor(() => expect(screen.queryByText('Edit')).toBeInTheDocument());
+    await waitFor(() =>
+      expect(screen.queryByText(SchemaCompText.Edit)).toBeInTheDocument()
+    );
   });
 });

+ 3 - 3
kafka-ui-react-app/src/components/Topics/List/List.styled.ts

@@ -2,10 +2,10 @@ import { Td } from 'components/common/table/TableHeaderCell/TableHeaderCell.styl
 import { NavLink } from 'react-router-dom';
 import styled, { css } from 'styled-components';
 
-export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })<{
+export const Link = styled(NavLink)<{
   $isInternal?: boolean;
 }>(
-  ({ theme, activeClassName, $isInternal }) => css`
+  ({ theme, $isInternal }) => css`
     color: ${theme.topicsList.color.normal};
     font-weight: 500;
     padding-left: ${$isInternal ? '5px' : 0};
@@ -15,7 +15,7 @@ export const Link = styled(NavLink).attrs({ activeClassName: 'is-active' })<{
       color: ${theme.topicsList.color.hover};
     }
 
-    &.${activeClassName} {
+    &.active {
       background-color: ${theme.topicsList.backgroundColor.active};
       color: ${theme.topicsList.color.active};
     }

+ 21 - 14
kafka-ui-react-app/src/components/Topics/List/List.tsx

@@ -1,11 +1,16 @@
 import React from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
 import {
   TopicWithDetailedInfo,
   ClusterName,
   TopicName,
 } from 'redux/interfaces';
-import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths';
+import {
+  ClusterNameRoute,
+  clusterTopicCopyRelativePath,
+  clusterTopicNewRelativePath,
+} from 'lib/paths';
 import usePagination from 'lib/hooks/usePagination';
 import useModal from 'lib/hooks/useModal';
 import ClusterContext from 'components/contexts/ClusterContext';
@@ -88,13 +93,15 @@ const List: React.FC<TopicsListProps> = ({
 }) => {
   const { isReadOnly, isTopicDeletionAllowed } =
     React.useContext(ClusterContext);
-  const { clusterName } = useParams<{ clusterName: ClusterName }>();
-  const { page, perPage, pathname } = usePagination();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
+  const { page, perPage } = usePagination();
   const [showInternal, setShowInternal] = React.useState<boolean>(
     !localStorage.getItem('hideInternalTopics') && true
   );
-  const [cachedPage, setCachedPage] = React.useState<number | null>(null);
-  const history = useHistory();
+  const [cachedPage, setCachedPage] = React.useState<number | null>(
+    page || null
+  );
+  const navigate = useNavigate();
 
   const topicsListParams = React.useMemo(
     () => ({
@@ -154,7 +161,9 @@ const List: React.FC<TopicsListProps> = ({
     }
 
     setShowInternal(!showInternal);
-    history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
+    navigate({
+      search: `?page=1&perPage=${perPage || PER_PAGE}`,
+    });
   };
 
   const [confirmationModal, setConfirmationModal] = React.useState<
@@ -176,9 +185,9 @@ const List: React.FC<TopicsListProps> = ({
 
     const newPageQuery = !searchString && cachedPage ? cachedPage : 1;
 
-    history.push(
-      `${pathname}?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`
-    );
+    navigate({
+      search: `?page=${newPageQuery}&perPage=${perPage || PER_PAGE}`,
+    });
   };
   const deleteOrPurgeConfirmationHandler = () => {
     const selectedIds = Array.from(tableState.selectedIds);
@@ -283,8 +292,7 @@ const List: React.FC<TopicsListProps> = ({
             <Button
               buttonType="primary"
               buttonSize="M"
-              isLink
-              to={clusterTopicNewPath(clusterName)}
+              to={clusterTopicNewRelativePath}
             >
               <i className="fas fa-plus" /> Add a Topic
             </Button>
@@ -331,9 +339,8 @@ const List: React.FC<TopicsListProps> = ({
                   <Button
                     buttonSize="M"
                     buttonType="secondary"
-                    isLink
                     to={{
-                      pathname: clusterTopicCopyPath(clusterName),
+                      pathname: clusterTopicCopyRelativePath,
                       search: `?${getSelectedTopic()}`,
                     }}
                   >

+ 1 - 1
kafka-ui-react-app/src/components/Topics/List/TopicsTableCells.tsx

@@ -12,7 +12,7 @@ export const TitleCell: React.FC<
   return (
     <>
       {internal && <Tag color="gray">IN</Tag>}
-      <S.Link exact to={`topics/${name}`} $isInternal={internal}>
+      <S.Link to={name} $isInternal={internal}>
         {name}
       </S.Link>
     </>

+ 64 - 57
kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx

@@ -1,17 +1,26 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
-import { screen, waitFor, within } from '@testing-library/react';
-import { Route, Router, StaticRouter } from 'react-router-dom';
+import { render, WithRoute } from 'lib/testHelpers';
+import { act, screen, waitFor, within } from '@testing-library/react';
 import ClusterContext, {
   ContextProps,
 } from 'components/contexts/ClusterContext';
 import List, { TopicsListProps } from 'components/Topics/List/List';
-import { createMemoryHistory } from 'history';
 import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
 import { CleanUpPolicy, SortOrder } from 'generated-sources';
 import userEvent from '@testing-library/user-event';
+import { clusterTopicsPath } from 'lib/paths';
+
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+}));
 
 describe('List', () => {
+  afterEach(() => {
+    mockNavigate.mockClear();
+  });
+
   const setupComponent = (props: Partial<TopicsListProps> = {}) => (
     <List
       areTopicsFetching={false}
@@ -32,15 +41,13 @@ describe('List', () => {
     />
   );
 
-  const historyMock = createMemoryHistory();
-
   const renderComponentWithProviders = (
     contextProps: Partial<ContextProps> = {},
     props: Partial<TopicsListProps> = {},
-    history = historyMock
+    queryParams = ''
   ) =>
     render(
-      <Router history={history}>
+      <WithRoute path={clusterTopicsPath()}>
         <ClusterContext.Provider
           value={{
             isReadOnly: true,
@@ -52,7 +59,8 @@ describe('List', () => {
         >
           {setupComponent(props)}
         </ClusterContext.Provider>
-      </Router>
+      </WithRoute>,
+      { initialEntries: [`${clusterTopicsPath('test')}${queryParams}`] }
     );
 
   describe('when it has readonly flag', () => {
@@ -112,6 +120,10 @@ describe('List', () => {
 
       await waitFor(() => {
         expect(fetchTopicsList).toHaveBeenLastCalledWith({
+          clusterName: 'test',
+          orderBy: undefined,
+          page: undefined,
+          perPage: undefined,
           search: '',
           showInternal: value === 'on',
           sortOrder: SortOrder.ASC,
@@ -119,47 +131,43 @@ describe('List', () => {
       });
     });
 
-    it('should reset page query param on show internal toggle change', () => {
-      const mockedHistory = createMemoryHistory();
-      jest.spyOn(mockedHistory, 'push');
-      renderComponentWithProviders(
-        { isReadOnly: false },
-        { fetchTopicsList },
-        mockedHistory
-      );
+    it('should reset page query param on show internal toggle change', async () => {
+      renderComponentWithProviders({ isReadOnly: false }, { fetchTopicsList });
 
       const internalCheckBox: HTMLInputElement = screen.getByRole('checkbox');
       userEvent.click(internalCheckBox);
 
-      expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25');
+      expect(mockNavigate).toHaveBeenCalledWith({
+        search: '?page=1&perPage=25',
+      });
     });
 
     it('should set cached page query param on show internal toggle change', async () => {
-      const mockedHistory = createMemoryHistory();
-      jest.spyOn(mockedHistory, 'push');
-
       const cachedPage = 5;
-      mockedHistory.push(`/?page=${cachedPage}&perPage=25`);
 
       renderComponentWithProviders(
         { isReadOnly: false },
         { fetchTopicsList, totalPages: 10 },
-        mockedHistory
+        `?page=${cachedPage}&perPage=25`
       );
 
       const searchInput = screen.getByPlaceholderText('Search by Topic Name');
       userEvent.type(searchInput, 'nonEmptyString');
 
       await waitFor(() => {
-        expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25');
+        expect(mockNavigate).toHaveBeenCalledWith({
+          search: '?page=1&perPage=25',
+        });
       });
 
-      userEvent.clear(searchInput);
+      await act(() => {
+        userEvent.clear(searchInput);
+      });
 
       await waitFor(() => {
-        expect(mockedHistory.push).toHaveBeenCalledWith(
-          `/?page=${cachedPage}&perPage=25`
-        );
+        expect(mockNavigate).toHaveBeenLastCalledWith({
+          search: `?page=${cachedPage}&perPage=25`,
+        });
       });
     });
   });
@@ -173,38 +181,37 @@ describe('List', () => {
     const fetchTopicsList = jest.fn();
 
     jest.useFakeTimers();
-    const pathname = '/ui/clusters/local/topics';
+    const pathname = clusterTopicsPath('local');
 
     beforeEach(() => {
       render(
-        <StaticRouter location={{ pathname }}>
-          <Route path="/ui/clusters/:clusterName">
-            <ClusterContext.Provider
-              value={{
-                isReadOnly: false,
-                hasKafkaConnectConfigured: true,
-                hasSchemaRegistryConfigured: true,
-                isTopicDeletionAllowed: true,
-              }}
-            >
-              {setupComponent({
-                topics: [
-                  {
-                    ...externalTopicPayload,
-                    cleanUpPolicy: CleanUpPolicy.DELETE,
-                  },
-                  { ...externalTopicPayload, name: 'external.topic2' },
-                ],
-                deleteTopics: mockDeleteTopics,
-                clearTopicsMessages: mockClearTopicsMessages,
-                recreateTopic: mockRecreate,
-                deleteTopic: mockDeleteTopic,
-                clearTopicMessages: mockClearTopic,
-                fetchTopicsList,
-              })}
-            </ClusterContext.Provider>
-          </Route>
-        </StaticRouter>
+        <WithRoute path={clusterTopicsPath()}>
+          <ClusterContext.Provider
+            value={{
+              isReadOnly: false,
+              hasKafkaConnectConfigured: true,
+              hasSchemaRegistryConfigured: true,
+              isTopicDeletionAllowed: true,
+            }}
+          >
+            {setupComponent({
+              topics: [
+                {
+                  ...externalTopicPayload,
+                  cleanUpPolicy: CleanUpPolicy.DELETE,
+                },
+                { ...externalTopicPayload, name: 'external.topic2' },
+              ],
+              deleteTopics: mockDeleteTopics,
+              clearTopicsMessages: mockClearTopicsMessages,
+              recreateTopic: mockRecreate,
+              deleteTopic: mockDeleteTopic,
+              clearTopicMessages: mockClearTopic,
+              fetchTopicsList,
+            })}
+          </ClusterContext.Provider>
+        </WithRoute>,
+        { initialEntries: [pathname] }
       );
     });
 

+ 8 - 10
kafka-ui-react-app/src/components/Topics/New/New.tsx

@@ -1,18 +1,15 @@
 import React from 'react';
-import { ClusterName, TopicFormData } from 'redux/interfaces';
+import { TopicFormData } from 'redux/interfaces';
 import { useForm, FormProvider } from 'react-hook-form';
-import { clusterTopicPath } from 'lib/paths';
+import { ClusterNameRoute } from 'lib/paths';
 import TopicForm from 'components/Topics/shared/Form/TopicForm';
+import { useNavigate, useLocation } from 'react-router-dom';
 import { createTopic } from 'redux/reducers/topics/topicsSlice';
-import { useHistory, useLocation, useParams } from 'react-router-dom';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { topicFormValidationSchema } from 'lib/yupExtended';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import { useAppDispatch } from 'lib/hooks/redux';
-
-interface RouterParams {
-  clusterName: ClusterName;
-}
+import useAppParams from 'lib/hooks/useAppParams';
 
 enum Filters {
   NAME = 'name',
@@ -28,8 +25,9 @@ const New: React.FC = () => {
     resolver: yupResolver(topicFormValidationSchema),
   });
 
-  const { clusterName } = useParams<RouterParams>();
-  const history = useHistory();
+  const { clusterName } = useAppParams<ClusterNameRoute>();
+  const navigate = useNavigate();
+
   const { search } = useLocation();
   const dispatch = useAppDispatch();
   const params = new URLSearchParams(search);
@@ -44,7 +42,7 @@ const New: React.FC = () => {
     const { meta } = await dispatch(createTopic({ clusterName, data }));
 
     if (meta.requestStatus === 'fulfilled') {
-      history.push(clusterTopicPath(clusterName, data.name));
+      navigate(`../${data.name}`);
     }
   };
 

+ 50 - 72
kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx

@@ -1,11 +1,10 @@
 import React from 'react';
 import New from 'components/Topics/New/New';
-import { Route, Router } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
 import configureStore from 'redux-mock-store';
 import { RootState } from 'redux/interfaces';
 import * as redux from 'react-redux';
 import { act, screen, waitFor } from '@testing-library/react';
-import { createMemoryHistory } from 'history';
 import fetchMock from 'fetch-mock-jest';
 import {
   clusterTopicCopyPath,
@@ -24,25 +23,37 @@ const topicName = 'test-topic';
 
 const initialState: Partial<RootState> = {};
 const storeMock = mockStore(initialState);
-const historyMock = createMemoryHistory();
 
-const renderComponent = (history = historyMock, store = storeMock) =>
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+}));
+
+const renderComponent = (path: string, store = storeMock) =>
   render(
-    <Router history={history}>
-      <Route path={clusterTopicNewPath(':clusterName')}>
-        <Provider store={store}>
-          <New />
-        </Provider>
-      </Route>
-      <Route path={clusterTopicCopyPath(':clusterName')}>
-        <Provider store={store}>
-          <New />
-        </Provider>
-      </Route>
-      <Route path={clusterTopicPath(':clusterName', ':topicName')}>
-        New topic path
-      </Route>
-    </Router>
+    <Routes>
+      <Route
+        path={clusterTopicNewPath()}
+        element={
+          <Provider store={store}>
+            <New />
+          </Provider>
+        }
+      />
+
+      <Route
+        path={clusterTopicCopyPath()}
+        element={
+          <Provider store={store}>
+            <New />
+          </Provider>
+        }
+      />
+
+      <Route path={clusterTopicPath()} element="New topic path" />
+    </Routes>,
+    { initialEntries: [path] }
   );
 
 describe('New', () => {
@@ -50,38 +61,26 @@ describe('New', () => {
     fetchMock.reset();
   });
 
+  afterEach(() => {
+    mockNavigate.mockClear();
+  });
+
   it('checks header for create new', async () => {
-    const mockedHistory = createMemoryHistory({
-      initialEntries: [clusterTopicNewPath(clusterName)],
-    });
-    renderComponent(mockedHistory);
+    renderComponent(clusterTopicNewPath(clusterName));
     expect(
       screen.getByRole('heading', { name: 'Create new Topic' })
     ).toHaveTextContent('Create new Topic');
   });
 
   it('checks header for copy', async () => {
-    const mockedHistory = createMemoryHistory({
-      initialEntries: [
-        {
-          pathname: clusterTopicCopyPath(clusterName),
-          search: `?name=test`,
-        },
-      ],
-    });
-
-    renderComponent(mockedHistory);
+    renderComponent(`${clusterTopicCopyPath(clusterName)}?name=test`);
     expect(
       screen.getByRole('heading', { name: 'Copy Topic' })
     ).toHaveTextContent('Copy Topic');
   });
 
   it('validates form', async () => {
-    const mockedHistory = createMemoryHistory({
-      initialEntries: [clusterTopicNewPath(clusterName)],
-    });
-    jest.spyOn(mockedHistory, 'push');
-    renderComponent(mockedHistory);
+    renderComponent(clusterTopicNewPath(clusterName));
 
     await waitFor(() => {
       userEvent.click(screen.getByText(/submit/i));
@@ -90,7 +89,7 @@ describe('New', () => {
       expect(screen.getByText('name is a required field')).toBeInTheDocument();
     });
     await waitFor(() => {
-      expect(mockedHistory.push).toBeCalledTimes(0);
+      expect(mockNavigate).not.toHaveBeenCalled();
     });
   });
 
@@ -101,14 +100,8 @@ describe('New', () => {
     })) as jest.Mock;
     useDispatchSpy.mockReturnValue(useDispatchMock);
 
-    const mockedHistory = createMemoryHistory({
-      initialEntries: [clusterTopicNewPath(clusterName)],
-    });
-
-    jest.spyOn(mockedHistory, 'push');
-
     await act(() => {
-      renderComponent(mockedHistory);
+      renderComponent(clusterTopicNewPath(clusterName));
     });
 
     await waitFor(() => {
@@ -116,14 +109,12 @@ describe('New', () => {
       userEvent.click(screen.getByText(/submit/i));
     });
 
-    await waitFor(() =>
-      expect(mockedHistory.location.pathname).toBe(
-        clusterTopicPath(clusterName, topicName)
-      )
-    );
+    await waitFor(() => {
+      expect(mockNavigate).toBeCalledTimes(1);
+      expect(mockNavigate).toHaveBeenLastCalledWith(`../${topicName}`);
+    });
 
     expect(useDispatchMock).toHaveBeenCalledTimes(1);
-    expect(mockedHistory.push).toBeCalledTimes(1);
   });
 
   it('does not redirect page when request is not fulfilled', async () => {
@@ -131,16 +122,11 @@ describe('New', () => {
     const useDispatchMock = jest.fn(() => ({
       meta: { requestStatus: 'pending' },
     })) as jest.Mock;
-    useDispatchSpy.mockReturnValue(useDispatchMock);
 
-    const mockedHistory = createMemoryHistory({
-      initialEntries: [clusterTopicNewPath(clusterName)],
-    });
-
-    jest.spyOn(mockedHistory, 'push');
+    useDispatchSpy.mockReturnValue(useDispatchMock);
 
     await act(() => {
-      renderComponent(mockedHistory);
+      renderComponent(clusterTopicNewPath(clusterName));
     });
 
     await waitFor(() => {
@@ -148,24 +134,16 @@ describe('New', () => {
       userEvent.click(screen.getByText(/submit/i));
     });
 
-    await waitFor(() =>
-      expect(mockedHistory.location.pathname).toBe(
-        clusterTopicNewPath(clusterName)
-      )
-    );
+    await waitFor(() => {
+      expect(mockNavigate).not.toHaveBeenCalled();
+    });
   });
 
   it('submits valid form that result in an error', async () => {
     const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
     const useDispatchMock = jest.fn();
     useDispatchSpy.mockReturnValue(useDispatchMock);
-
-    const mockedHistory = createMemoryHistory({
-      initialEntries: [clusterTopicNewPath(clusterName)],
-    });
-
-    jest.spyOn(mockedHistory, 'push');
-    renderComponent(mockedHistory);
+    renderComponent(clusterTopicNewPath(clusterName));
 
     await act(() => {
       userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
@@ -173,6 +151,6 @@ describe('New', () => {
     });
 
     expect(useDispatchMock).toHaveBeenCalledTimes(1);
-    expect(mockedHistory.push).toBeCalledTimes(0);
+    expect(mockNavigate).not.toHaveBeenCalled();
   });
 });

+ 12 - 10
kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups.tsx

@@ -1,19 +1,18 @@
 import React from 'react';
-import { Topic, TopicDetails, ConsumerGroup } from 'generated-sources';
+import { Link } from 'react-router-dom';
 import { ClusterName, TopicName } from 'redux/interfaces';
-import { clusterConsumerGroupsPath } from 'lib/paths';
+import { clusterConsumerGroupsPath, RouteParamsClusterTopic } from 'lib/paths';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
-import { Link } from 'react-router-dom';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import getTagColor from 'components/common/Tag/getTagColor';
+import { useAppSelector } from 'lib/hooks/redux';
+import { getTopicConsumerGroups } from 'redux/reducers/topics/selectors';
+import useAppParams from 'lib/hooks/useAppParams';
 
-export interface Props extends Topic, TopicDetails {
-  clusterName: ClusterName;
-  topicName: TopicName;
-  consumerGroups: ConsumerGroup[];
+export interface Props {
   isFetched: boolean;
   fetchTopicConsumerGroups(payload: {
     clusterName: ClusterName;
@@ -22,12 +21,15 @@ export interface Props extends Topic, TopicDetails {
 }
 
 const TopicConsumerGroups: React.FC<Props> = ({
-  consumerGroups,
   fetchTopicConsumerGroups,
-  clusterName,
-  topicName,
   isFetched,
 }) => {
+  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
+
+  const consumerGroups = useAppSelector((state) =>
+    getTopicConsumerGroups(state, topicName)
+  );
+
   React.useEffect(() => {
     fetchTopicConsumerGroups({ clusterName, topicName });
   }, [clusterName, fetchTopicConsumerGroups, topicName]);

+ 7 - 27
kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroupsContainer.ts

@@ -1,31 +1,10 @@
 import { connect } from 'react-redux';
-import { RootState, TopicName, ClusterName } from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RootState } from 'redux/interfaces';
 import { fetchTopicConsumerGroups } from 'redux/reducers/topics/topicsSlice';
 import TopicConsumerGroups from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups';
-import {
-  getTopicConsumerGroups,
-  getTopicsConsumerGroupsFetched,
-} from 'redux/reducers/topics/selectors';
+import { getTopicsConsumerGroupsFetched } from 'redux/reducers/topics/selectors';
 
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-  }: OwnProps
-) => ({
-  consumerGroups: getTopicConsumerGroups(state, topicName),
-  topicName,
-  clusterName,
+const mapStateToProps = (state: RootState) => ({
   isFetched: getTopicsConsumerGroupsFetched(state),
 });
 
@@ -33,6 +12,7 @@ const mapDispatchToProps = {
   fetchTopicConsumerGroups,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(TopicConsumerGroups)
-);
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(TopicConsumerGroups);

+ 42 - 17
kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/__test__/TopicConsumerGroups.spec.tsx

@@ -1,10 +1,13 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { screen } from '@testing-library/react';
-import ConsumerGroups, {
+import TopicConsumerGroups, {
   Props,
 } from 'components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups';
-import { ConsumerGroupState } from 'generated-sources';
+import { ConsumerGroup, ConsumerGroupState } from 'generated-sources';
+import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures';
+import { TopicWithDetailedInfo } from 'redux/interfaces';
+import { clusterTopicConsumerGroupsPath } from 'lib/paths';
 
 describe('TopicConsumerGroups', () => {
   const mockClusterName = 'localClusterName';
@@ -32,18 +35,32 @@ describe('TopicConsumerGroups', () => {
     },
   ];
 
-  const setUpComponent = (props: Partial<Props> = {}) => {
-    const { name, topicName, consumerGroups, isFetched } = props;
+  const setUpComponent = (
+    props: Partial<Props> = {},
+    consumerGroups?: ConsumerGroup[]
+  ) => {
+    const topic: TopicWithDetailedInfo = {
+      name: mockTopicName,
+      consumerGroups,
+    };
+    const topicsState = getTopicStateFixtures([topic]);
 
     return render(
-      <ConsumerGroups
-        clusterName={mockClusterName}
-        consumerGroups={consumerGroups?.length ? consumerGroups : []}
-        name={name || mockTopicName}
-        fetchTopicConsumerGroups={jest.fn()}
-        topicName={topicName || mockTopicName}
-        isFetched={'isFetched' in props ? !!isFetched : false}
-      />
+      <WithRoute path={clusterTopicConsumerGroupsPath()}>
+        <TopicConsumerGroups
+          fetchTopicConsumerGroups={jest.fn()}
+          isFetched={false}
+          {...props}
+        />
+      </WithRoute>,
+      {
+        initialEntries: [
+          clusterTopicConsumerGroupsPath(mockClusterName, mockTopicName),
+        ],
+        preloadedState: {
+          topics: topicsState,
+        },
+      }
     );
   };
 
@@ -62,10 +79,18 @@ describe('TopicConsumerGroups', () => {
   });
 
   it('render ConsumerGroups in Topic', () => {
-    setUpComponent({
-      consumerGroups: mockWithConsumerGroup,
-      isFetched: true,
-    });
+    setUpComponent(
+      {
+        isFetched: true,
+      },
+      mockWithConsumerGroup
+    );
     expect(screen.getAllByRole('rowgroup')).toHaveLength(2);
+    expect(
+      screen.getByText(mockWithConsumerGroup[0].groupId)
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText(mockWithConsumerGroup[1].groupId)
+    ).toBeInTheDocument();
   });
 });

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.styled.ts

@@ -8,7 +8,7 @@ export const DropdownExtraMessage = styled.div`
 `;
 
 export const ReplicaCell = styled.span.attrs({ 'aria-label': 'replica-info' })<{
-  leader: boolean | undefined;
+  leader?: boolean;
 }>`
   ${this} ~ ${this}::before {
     color: black;

+ 101 - 102
kafka-ui-react-app/src/components/Topics/Topic/Details/Details.tsx

@@ -1,15 +1,13 @@
 import React from 'react';
 import { ClusterName, TopicName } from 'redux/interfaces';
-import { Topic, TopicDetails } from 'generated-sources';
-import { NavLink, Switch, Route, useHistory } from 'react-router-dom';
+import { NavLink, Route, Routes, useNavigate } from 'react-router-dom';
 import {
-  clusterTopicSettingsPath,
-  clusterTopicPath,
-  clusterTopicMessagesPath,
-  clusterTopicsPath,
-  clusterTopicConsumerGroupsPath,
-  clusterTopicEditPath,
-  clusterTopicSendMessagePath,
+  RouteParamsClusterTopic,
+  clusterTopicMessagesRelativePath,
+  clusterTopicSettingsRelativePath,
+  clusterTopicConsumerGroupsRelativePath,
+  clusterTopicEditRelativePath,
+  clusterTopicSendMessageRelativePath,
 } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
@@ -22,18 +20,20 @@ import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import styled from 'styled-components';
 import Navbar from 'components/common/Navigation/Navbar.styled';
 import * as S from 'components/Topics/Topic/Details/Details.styled';
+import { useAppSelector } from 'lib/hooks/redux';
+import {
+  getIsTopicDeletePolicy,
+  getIsTopicInternal,
+} from 'redux/reducers/topics/selectors';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import OverviewContainer from './Overview/OverviewContainer';
 import TopicConsumerGroupsContainer from './ConsumerGroups/TopicConsumerGroupsContainer';
 import SettingsContainer from './Settings/SettingsContainer';
 import Messages from './Messages/Messages';
 
-interface Props extends Topic, TopicDetails {
-  clusterName: ClusterName;
-  topicName: TopicName;
-  isInternal: boolean;
+interface Props {
   isDeleted: boolean;
-  isDeletePolicy: boolean;
   deleteTopic: (payload: {
     clusterName: ClusterName;
     topicName: TopicName;
@@ -56,16 +56,22 @@ const HeaderControlsWrapper = styled.div`
 `;
 
 const Details: React.FC<Props> = ({
-  clusterName,
-  topicName,
-  isInternal,
   isDeleted,
-  isDeletePolicy,
   deleteTopic,
   recreateTopic,
   clearTopicMessages,
 }) => {
-  const history = useHistory();
+  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
+
+  const isInternal = useAppSelector((state) =>
+    getIsTopicInternal(state, topicName)
+  );
+
+  const isDeletePolicy = useAppSelector((state) =>
+    getIsTopicDeletePolicy(state, topicName)
+  );
+
+  const navigate = useNavigate();
   const dispatch = useDispatch();
   const { isReadOnly, isTopicDeletionAllowed } =
     React.useContext(ClusterContext);
@@ -81,9 +87,9 @@ const Details: React.FC<Props> = ({
 
   React.useEffect(() => {
     if (isDeleted) {
-      history.push(clusterTopicsPath(clusterName));
+      navigate('../..');
     }
-  }, [isDeleted, clusterName, dispatch, history]);
+  }, [isDeleted, clusterName, dispatch, navigate]);
 
   const clearTopicMessagesHandler = () => {
     clearTopicMessages({ clusterName, topicName });
@@ -99,58 +105,62 @@ const Details: React.FC<Props> = ({
     <div>
       <PageHeading text={topicName}>
         <HeaderControlsWrapper>
-          <Route
-            exact
-            path="/ui/clusters/:clusterName/topics/:topicName/messages"
-          >
-            <Button
-              buttonSize="M"
-              buttonType="primary"
-              isLink
-              to={clusterTopicSendMessagePath(clusterName, topicName)}
-            >
-              Produce Message
-            </Button>
-          </Route>
-          {!isReadOnly && !isInternal && (
-            <Route path="/ui/clusters/:clusterName/topics/:topicName">
-              <Dropdown label={<VerticalElipsisIcon />} right>
-                <DropdownItem
-                  onClick={() =>
-                    history.push(clusterTopicEditPath(clusterName, topicName))
-                  }
-                >
-                  Edit settings
-                  <S.DropdownExtraMessage>
-                    Pay attention! This operation has
-                    <br />
-                    especially important consequences.
-                  </S.DropdownExtraMessage>
-                </DropdownItem>
-                {isDeletePolicy && (
-                  <DropdownItem
-                    onClick={() => setClearTopicConfirmationVisible(true)}
-                    danger
-                  >
-                    Clear messages
-                  </DropdownItem>
-                )}
-                <DropdownItem
-                  onClick={() => setRecreateTopicConfirmationVisible(true)}
-                  danger
+          <Routes>
+            <Route
+              path={clusterTopicMessagesRelativePath}
+              element={
+                <Button
+                  buttonSize="M"
+                  buttonType="primary"
+                  to={`../${clusterTopicSendMessageRelativePath}`}
                 >
-                  Recreate Topic
-                </DropdownItem>
-                {isTopicDeletionAllowed && (
-                  <DropdownItem
-                    onClick={() => setDeleteTopicConfirmationVisible(true)}
-                    danger
-                  >
-                    Remove topic
-                  </DropdownItem>
-                )}
-              </Dropdown>
-            </Route>
+                  Produce Message
+                </Button>
+              }
+            />
+          </Routes>
+          {!isReadOnly && !isInternal && (
+            <Routes>
+              <Route
+                index
+                element={
+                  <Dropdown label={<VerticalElipsisIcon />} right>
+                    <DropdownItem
+                      onClick={() => navigate(clusterTopicEditRelativePath)}
+                    >
+                      Edit settings
+                      <S.DropdownExtraMessage>
+                        Pay attention! This operation has
+                        <br />
+                        especially important consequences.
+                      </S.DropdownExtraMessage>
+                    </DropdownItem>
+                    {isDeletePolicy && (
+                      <DropdownItem
+                        onClick={() => setClearTopicConfirmationVisible(true)}
+                        danger
+                      >
+                        Clear messages
+                      </DropdownItem>
+                    )}
+                    <DropdownItem
+                      onClick={() => setRecreateTopicConfirmationVisible(true)}
+                      danger
+                    >
+                      Recreate Topic
+                    </DropdownItem>
+                    {isTopicDeletionAllowed && (
+                      <DropdownItem
+                        onClick={() => setDeleteTopicConfirmationVisible(true)}
+                        danger
+                      >
+                        Remove topic
+                      </DropdownItem>
+                    )}
+                  </Dropdown>
+                }
+              />
+            </Routes>
           )}
         </HeaderControlsWrapper>
       </PageHeading>
@@ -177,56 +187,45 @@ const Details: React.FC<Props> = ({
       </ConfirmationModal>
       <Navbar role="navigation">
         <NavLink
-          exact
-          to={clusterTopicPath(clusterName, topicName)}
-          activeClassName="is-active is-primary"
+          to="."
+          className={({ isActive }) => (isActive ? 'is-active is-primary' : '')}
         >
           Overview
         </NavLink>
         <NavLink
-          exact
-          to={clusterTopicMessagesPath(clusterName, topicName)}
-          activeClassName="is-active"
+          to={clusterTopicMessagesRelativePath}
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
         >
           Messages
         </NavLink>
         <NavLink
-          exact
-          to={clusterTopicConsumerGroupsPath(clusterName, topicName)}
-          activeClassName="is-active"
+          to={clusterTopicConsumerGroupsRelativePath}
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
         >
           Consumers
         </NavLink>
         <NavLink
-          exact
-          to={clusterTopicSettingsPath(clusterName, topicName)}
-          activeClassName="is-active"
+          to={clusterTopicSettingsRelativePath}
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
         >
           Settings
         </NavLink>
       </Navbar>
-      <Switch>
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/:topicName/messages"
-          component={Messages}
-        />
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/:topicName/settings"
-          component={SettingsContainer}
-        />
+      <Routes>
+        <Route index element={<OverviewContainer />} />
+
+        <Route path={clusterTopicMessagesRelativePath} element={<Messages />} />
+
         <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/:topicName"
-          component={OverviewContainer}
+          path={clusterTopicSettingsRelativePath}
+          element={<SettingsContainer />}
         />
+
         <Route
-          exact
-          path="/ui/clusters/:clusterName/topics/:topicName/consumer-groups"
-          component={TopicConsumerGroupsContainer}
+          path={clusterTopicConsumerGroupsRelativePath}
+          element={<TopicConsumerGroupsContainer />}
         />
-      </Switch>
+      </Routes>
     </div>
   );
 };

+ 4 - 29
kafka-ui-react-app/src/components/Topics/Topic/Details/DetailsContainer.ts

@@ -1,36 +1,13 @@
 import { connect } from 'react-redux';
-import { ClusterName, RootState, TopicName } from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RootState } from 'redux/interfaces';
 import { deleteTopic, recreateTopic } from 'redux/reducers/topics/topicsSlice';
 import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
-import {
-  getIsTopicDeleted,
-  getIsTopicDeletePolicy,
-  getIsTopicInternal,
-} from 'redux/reducers/topics/selectors';
+import { getIsTopicDeleted } from 'redux/reducers/topics/selectors';
 
 import Details from './Details';
 
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  topicName,
-  isInternal: getIsTopicInternal(state, topicName),
+const mapStateToProps = (state: RootState) => ({
   isDeleted: getIsTopicDeleted(state),
-  isDeletePolicy: getIsTopicDeletePolicy(state, topicName),
 });
 
 const mapDispatchToProps = {
@@ -39,6 +16,4 @@ const mapDispatchToProps = {
   clearTopicMessages,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Details)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(Details);

+ 13 - 10
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx

@@ -12,12 +12,11 @@ import {
 } from 'generated-sources';
 import React, { useContext } from 'react';
 import { omitBy } from 'lodash';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useNavigate, useLocation } from 'react-router-dom';
 import DatePicker from 'react-datepicker';
 import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
 import { Option } from 'react-multi-select-component/dist/lib/interfaces';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
-import { ClusterName, TopicName } from 'redux/interfaces';
 import { BASE_PARAMS } from 'lib/constants';
 import Input from 'components/common/Input/Input';
 import Select from 'components/common/Select/Select';
@@ -29,6 +28,10 @@ import FilterModal, {
 import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
 import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
 import useModal from 'lib/hooks/useModal';
+import { getPartitionsByTopicName } from 'redux/reducers/topics/selectors';
+import { useAppSelector } from 'lib/hooks/redux';
+import { RouteParamsClusterTopic } from 'lib/paths';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import * as S from './Filters.styled';
 import {
@@ -41,10 +44,7 @@ import {
 type Query = Record<string, string | string[] | number>;
 
 export interface FiltersProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
   phaseMessage?: string;
-  partitions: Partition[];
   meta: TopicMessageConsuming;
   isFetching: boolean;
   addMessage(content: { message: TopicMessage; prepend: boolean }): void;
@@ -73,9 +73,6 @@ export const SeekTypeOptions = [
 ];
 
 const Filters: React.FC<FiltersProps> = ({
-  clusterName,
-  topicName,
-  partitions,
   phaseMessage,
   meta: { elapsedMs, bytesConsumed, messagesConsumed },
   isFetching,
@@ -85,8 +82,13 @@ const Filters: React.FC<FiltersProps> = ({
   updateMeta,
   setIsFetching,
 }) => {
+  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
   const location = useLocation();
-  const history = useHistory();
+  const navigate = useNavigate();
+
+  const partitions = useAppSelector((state) =>
+    getPartitionsByTopicName(state, topicName)
+  );
 
   const { searchParams, seekDirection, isLive, changeSeekDirection } =
     useContext(TopicMessagesContext);
@@ -212,7 +214,7 @@ const Filters: React.FC<FiltersProps> = ({
         .map((key) => `${key}=${newProps[key]}`)
         .join('&');
 
-      history.push({
+      navigate({
         search: `?${qs}`,
       });
     },
@@ -224,6 +226,7 @@ const Filters: React.FC<FiltersProps> = ({
       timestamp,
       query,
       selectedPartitions,
+      navigate,
     ]
   );
 

+ 3 - 24
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/FiltersContainer.ts

@@ -1,6 +1,5 @@
 import { connect } from 'react-redux';
-import { ClusterName, RootState, TopicName } from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RootState } from 'redux/interfaces';
 import {
   addTopicMessage,
   resetTopicMessages,
@@ -13,29 +12,11 @@ import {
   getTopicMessgesPhase,
   getIsTopicMessagesFetching,
 } from 'redux/reducers/topicMessages/selectors';
-import { getPartitionsByTopicName } from 'redux/reducers/topics/selectors';
 
 import Filters from './Filters';
 
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  topicName,
+const mapStateToProps = (state: RootState) => ({
   phaseMessage: getTopicMessgesPhase(state),
-  partitions: getPartitionsByTopicName(state, topicName),
   meta: getTopicMessgesMeta(state),
   isFetching: getIsTopicMessagesFetching(state),
 });
@@ -48,6 +29,4 @@ const mapDispatchToProps = {
   setIsFetching: setTopicMessagesFetchingStatus,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Filters)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(Filters);

+ 5 - 4
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx

@@ -4,7 +4,7 @@ import Filters, {
   FiltersProps,
   SeekTypeOptions,
 } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
-import { render } from 'lib/testHelpers';
+import { EventSourceMock, render } from 'lib/testHelpers';
 import { act, screen, within, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import TopicMessagesContext, {
@@ -26,9 +26,6 @@ const renderComponent = (
   render(
     <TopicMessagesContext.Provider value={ctx}>
       <Filters
-        clusterName="test-cluster"
-        topicName="test-topic"
-        partitions={[{ partition: 0, offsetMin: 0, offsetMax: 100 }]}
         meta={{}}
         isFetching={false}
         addMessage={jest.fn()}
@@ -43,6 +40,10 @@ const renderComponent = (
 };
 
 describe('Filters component', () => {
+  Object.defineProperty(window, 'EventSource', {
+    value: EventSourceMock,
+  });
+
   it('shows cancel button while fetching', () => {
     renderComponent({ isFetching: true });
     expect(screen.getByText('Cancel')).toBeInTheDocument();

+ 2 - 10
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx

@@ -5,8 +5,6 @@ import Messages, {
   SeekDirectionOptions,
   SeekDirectionOptionsObj,
 } from 'components/Topics/Topic/Details/Messages/Messages';
-import { Router } from 'react-router-dom';
-import { createMemoryHistory } from 'history';
 import { SeekDirection, SeekType } from 'generated-sources';
 import userEvent from '@testing-library/user-event';
 
@@ -14,15 +12,9 @@ describe('Messages', () => {
   const searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`;
 
   const setUpComponent = (param: string = searchParams) => {
-    const history = createMemoryHistory();
-    history.push({
-      search: new URLSearchParams(param).toString(),
+    return render(<Messages />, {
+      initialEntries: [`/?${new URLSearchParams(param).toString()}`],
     });
-    return render(
-      <Router history={history}>
-        <Messages />
-      </Router>
-    );
   };
 
   beforeEach(() => {

+ 12 - 13
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx

@@ -2,8 +2,6 @@ import React from 'react';
 import { screen } from '@testing-library/react';
 import { render } from 'lib/testHelpers';
 import MessagesTable from 'components/Topics/Topic/Details/Messages/MessagesTable';
-import { Router } from 'react-router-dom';
-import { createMemoryHistory, MemoryHistory } from 'history';
 import { SeekDirection, SeekType, TopicMessage } from 'generated-sources';
 import TopicMessagesContext, {
   ContextProps,
@@ -15,6 +13,12 @@ import {
 
 const mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }];
 
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+}));
+
 describe('MessagesTable', () => {
   const seekToResult = '&seekTo=0::9';
   const searchParamsValue = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}${seekToResult}`;
@@ -31,20 +35,15 @@ describe('MessagesTable', () => {
     ctx: ContextProps = contextValue,
     messages: TopicMessage[] = [],
     isFetching?: boolean,
-    customHistory?: MemoryHistory
+    path?: string
   ) => {
-    const history =
-      customHistory ||
-      createMemoryHistory({
-        initialEntries: [params.toString()],
-      });
+    const customPath = path || params.toString();
     return render(
-      <Router history={history}>
-        <TopicMessagesContext.Provider value={ctx}>
-          <MessagesTable />
-        </TopicMessagesContext.Provider>
-      </Router>,
+      <TopicMessagesContext.Provider value={ctx}>
+        <MessagesTable />
+      </TopicMessagesContext.Provider>,
       {
+        initialEntries: [customPath],
         preloadedState: {
           topicMessages: {
             messages,

+ 25 - 19
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { Partition, Replica, Topic, TopicDetails } from 'generated-sources';
+import { Partition, Replica } from 'generated-sources';
 import { ClusterName, TopicName } from 'redux/interfaces';
 import Dropdown from 'components/common/Dropdown/Dropdown';
 import DropdownItem from 'components/common/Dropdown/DropdownItem';
@@ -10,11 +10,13 @@ import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeader
 import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import * as Metrics from 'components/common/Metrics';
 import { Tag } from 'components/common/Tag/Tag.styled';
+import { useAppSelector } from 'lib/hooks/redux';
+import { getTopicByName } from 'redux/reducers/topics/selectors';
 import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
+import { RouteParamsClusterTopic } from 'lib/paths';
+import useAppParams from 'lib/hooks/useAppParams';
 
-export interface Props extends Topic, TopicDetails {
-  clusterName: ClusterName;
-  topicName: TopicName;
+export interface Props {
   clearTopicMessages(params: {
     clusterName: ClusterName;
     topicName: TopicName;
@@ -22,21 +24,25 @@ export interface Props extends Topic, TopicDetails {
   }): void;
 }
 
-const Overview: React.FC<Props> = ({
-  partitions,
-  underReplicatedPartitions,
-  inSyncReplicas,
-  replicas,
-  partitionCount,
-  internal,
-  replicationFactor,
-  segmentSize,
-  segmentCount,
-  clusterName,
-  topicName,
-  cleanUpPolicy,
-  clearTopicMessages,
-}) => {
+const Overview: React.FC<Props> = ({ clearTopicMessages }) => {
+  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
+
+  const {
+    partitions,
+    underReplicatedPartitions,
+    inSyncReplicas,
+    replicas,
+    partitionCount,
+    internal,
+    replicationFactor,
+    segmentSize,
+    segmentCount,
+    cleanUpPolicy,
+  } = useAppSelector((state) => {
+    const res = getTopicByName(state, topicName);
+    return res || {};
+  });
+
   const { isReadOnly } = React.useContext(ClusterContext);
 
   const messageCount = React.useMemo(

+ 1 - 26
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/OverviewContainer.ts

@@ -1,34 +1,9 @@
 import { connect } from 'react-redux';
-import { RootState, TopicName, ClusterName } from 'redux/interfaces';
-import { getTopicByName } from 'redux/reducers/topics/selectors';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
 import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
 import Overview from 'components/Topics/Topic/Details/Overview/Overview';
 
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-  }: OwnProps
-) => ({
-  ...getTopicByName(state, topicName),
-  topicName,
-  clusterName,
-});
-
 const mapDispatchToProps = {
   clearTopicMessages,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Overview)
-);
+export default connect(null, mapDispatchToProps)(Overview);

+ 96 - 80
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx

@@ -1,20 +1,22 @@
 import React from 'react';
 import { screen } from '@testing-library/react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import Overview, {
   Props as OverviewProps,
 } from 'components/Topics/Topic/Details/Overview/Overview';
 import theme from 'theme/theme';
-import { CleanUpPolicy } from 'generated-sources';
+import { CleanUpPolicy, Topic } from 'generated-sources';
 import ClusterContext from 'components/contexts/ClusterContext';
 import userEvent from '@testing-library/user-event';
+import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures';
+import { clusterTopicPath } from 'lib/paths';
 import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
 
 describe('Overview', () => {
-  const getReplicaCell = () => screen.getByLabelText('replica-info');
   const mockClusterName = 'local';
   const mockTopicName = 'topic';
-  const mockClearTopicMessages = jest.fn();
+  const mockTopic = { name: mockTopicName };
+
   const mockPartitions = [
     {
       partition: 1,
@@ -36,67 +38,63 @@ describe('Overview', () => {
     hasSchemaRegistryConfigured: true,
     isTopicDeletionAllowed: true,
   };
-  const defaultProps: OverviewProps = {
-    name: mockTopicName,
-    partitions: [],
-    internal: true,
-    clusterName: mockClusterName,
-    topicName: mockTopicName,
-    clearTopicMessages: mockClearTopicMessages,
-  };
 
   const setupComponent = (
-    props = defaultProps,
-    contextValues = defaultContextValues,
-    underReplicatedPartitions?: number,
-    inSyncReplicas?: number,
-    replicas?: number
+    props: Partial<OverviewProps> = {},
+    topicState: Topic = mockTopic,
+    contextValues = defaultContextValues
   ) => {
+    const topics = getTopicStateFixtures([topicState]);
+
     return render(
-      <ClusterContext.Provider value={contextValues}>
-        <Overview
-          underReplicatedPartitions={underReplicatedPartitions}
-          inSyncReplicas={inSyncReplicas}
-          replicas={replicas}
-          {...props}
-        />
-      </ClusterContext.Provider>
+      <WithRoute path={clusterTopicPath()}>
+        <ClusterContext.Provider value={contextValues}>
+          <Overview clearTopicMessages={jest.fn()} {...props} />
+        </ClusterContext.Provider>
+      </WithRoute>,
+      {
+        initialEntries: [clusterTopicPath(mockClusterName, mockTopicName)],
+        preloadedState: { topics },
+      }
     );
   };
 
-  afterEach(() => {
-    mockClearTopicMessages.mockClear();
-  });
-
   it('at least one replica was rendered', () => {
-    setupComponent({
-      ...defaultProps,
-      underReplicatedPartitions: 0,
-      inSyncReplicas: 1,
-      replicas: 1,
-    });
-    expect(getReplicaCell()).toBeInTheDocument();
+    setupComponent(
+      {},
+      {
+        ...mockTopic,
+        partitions: mockPartitions,
+        internal: false,
+        cleanUpPolicy: CleanUpPolicy.DELETE,
+      }
+    );
+    expect(screen.getByLabelText('replica-info')).toBeInTheDocument();
   });
 
   it('renders replica cell with props', () => {
     render(<ReplicaCell leader />);
-    expect(getReplicaCell()).toBeInTheDocument();
-    expect(getReplicaCell()).toHaveStyleRule('color', 'orange');
+    const element = screen.getByLabelText('replica-info');
+    expect(element).toBeInTheDocument();
+    expect(element).toHaveStyleRule('color', 'orange');
   });
 
   describe('when it has internal flag', () => {
     it('does not render the Action button a Topic', () => {
-      setupComponent({
-        ...defaultProps,
-        partitions: mockPartitions,
-        internal: false,
-        cleanUpPolicy: CleanUpPolicy.DELETE,
-      });
+      setupComponent(
+        {},
+        {
+          ...mockTopic,
+          partitions: mockPartitions,
+          internal: false,
+          cleanUpPolicy: CleanUpPolicy.DELETE,
+        }
+      );
       expect(screen.getAllByRole('menu')[0]).toBeInTheDocument();
     });
 
     it('does not render Partitions', () => {
-      setupComponent();
+      setupComponent({}, { ...mockTopic, partitions: [] });
 
       expect(screen.getByText('No Partitions found')).toBeInTheDocument();
     });
@@ -110,12 +108,15 @@ describe('Overview', () => {
     });
 
     it('should be the appropriate color', () => {
-      setupComponent({
-        ...defaultProps,
-        underReplicatedPartitions: 0,
-        inSyncReplicas: 1,
-        replicas: 2,
-      });
+      setupComponent(
+        {},
+        {
+          ...mockTopic,
+          underReplicatedPartitions: 0,
+          inSyncReplicas: 1,
+          replicas: 2,
+        }
+      );
       const circles = screen.getAllByRole('circle');
       expect(circles[0]).toHaveStyle(
         `fill: ${theme.circularAlert.color.success}`
@@ -127,24 +128,30 @@ describe('Overview', () => {
   });
 
   describe('when Clear Messages is clicked', () => {
-    setupComponent({
-      ...defaultProps,
-      partitions: mockPartitions,
-      internal: false,
-      cleanUpPolicy: CleanUpPolicy.DELETE,
-    });
-
-    const clearMessagesButton = screen.getByText('Clear Messages');
-    userEvent.click(clearMessagesButton);
+    it('should when Clear Messages is clicked', () => {
+      const mockClearTopicMessages = jest.fn();
+      setupComponent(
+        { clearTopicMessages: mockClearTopicMessages },
+        {
+          ...mockTopic,
+          partitions: mockPartitions,
+          internal: false,
+          cleanUpPolicy: CleanUpPolicy.DELETE,
+        }
+      );
 
-    expect(mockClearTopicMessages).toHaveBeenCalledTimes(1);
+      const clearMessagesButton = screen.getByText('Clear Messages');
+      userEvent.click(clearMessagesButton);
+      expect(mockClearTopicMessages).toHaveBeenCalledTimes(1);
+    });
   });
 
   describe('when the table partition dropdown appearance', () => {
     it('should check if the dropdown is not present when it is readOnly', () => {
       setupComponent(
+        {},
         {
-          ...defaultProps,
+          ...mockTopic,
           partitions: mockPartitions,
           internal: true,
           cleanUpPolicy: CleanUpPolicy.DELETE,
@@ -155,32 +162,41 @@ describe('Overview', () => {
     });
 
     it('should check if the dropdown is not present when it is internal', () => {
-      setupComponent({
-        ...defaultProps,
-        partitions: mockPartitions,
-        internal: true,
-        cleanUpPolicy: CleanUpPolicy.DELETE,
-      });
+      setupComponent(
+        {},
+        {
+          ...mockTopic,
+          partitions: mockPartitions,
+          internal: true,
+          cleanUpPolicy: CleanUpPolicy.DELETE,
+        }
+      );
       expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument();
     });
 
     it('should check if the dropdown is not present when cleanUpPolicy is not DELETE', () => {
-      setupComponent({
-        ...defaultProps,
-        partitions: mockPartitions,
-        internal: false,
-        cleanUpPolicy: CleanUpPolicy.COMPACT,
-      });
+      setupComponent(
+        {},
+        {
+          ...mockTopic,
+          partitions: mockPartitions,
+          internal: false,
+          cleanUpPolicy: CleanUpPolicy.COMPACT,
+        }
+      );
       expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument();
     });
 
     it('should check if the dropdown action to be in visible', () => {
-      setupComponent({
-        ...defaultProps,
-        partitions: mockPartitions,
-        internal: false,
-        cleanUpPolicy: CleanUpPolicy.DELETE,
-      });
+      setupComponent(
+        {},
+        {
+          ...mockTopic,
+          partitions: mockPartitions,
+          internal: false,
+          cleanUpPolicy: CleanUpPolicy.DELETE,
+        }
+      );
       expect(screen.getByText('Clear Messages')).toBeInTheDocument();
     });
   });

+ 11 - 13
kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/Settings.tsx

@@ -1,16 +1,16 @@
+import React from 'react';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
-import { TopicConfig } from 'generated-sources';
-import React from 'react';
 import { ClusterName, TopicName } from 'redux/interfaces';
+import { useAppSelector } from 'lib/hooks/redux';
+import { getTopicConfig } from 'redux/reducers/topics/selectors';
+import { RouteParamsClusterTopic } from 'lib/paths';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import ConfigListItem from './ConfigListItem';
 
-interface Props {
-  clusterName: ClusterName;
-  topicName: TopicName;
-  config?: TopicConfig[];
+export interface Props {
   isFetched: boolean;
   fetchTopicConfig: (payload: {
     clusterName: ClusterName;
@@ -18,13 +18,11 @@ interface Props {
   }) => void;
 }
 
-const Settings: React.FC<Props> = ({
-  clusterName,
-  topicName,
-  isFetched,
-  fetchTopicConfig,
-  config,
-}) => {
+const Settings: React.FC<Props> = ({ isFetched, fetchTopicConfig }) => {
+  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
+
+  const config = useAppSelector((state) => getTopicConfig(state, topicName));
+
   React.useEffect(() => {
     fetchTopicConfig({ clusterName, topicName });
   }, [fetchTopicConfig, clusterName, topicName]);

+ 4 - 27
kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/SettingsContainer.ts

@@ -1,32 +1,11 @@
 import { connect } from 'react-redux';
-import { RootState, ClusterName, TopicName } from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RootState } from 'redux/interfaces';
 import { fetchTopicConfig } from 'redux/reducers/topics/topicsSlice';
-import {
-  getTopicConfig,
-  getTopicConfigFetched,
-} from 'redux/reducers/topics/selectors';
+import { getTopicConfigFetched } from 'redux/reducers/topics/selectors';
 
 import Settings from './Settings';
 
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  topicName,
-  config: getTopicConfig(state, topicName),
+const mapStateToProps = (state: RootState) => ({
   isFetched: getTopicConfigFetched(state),
 });
 
@@ -34,6 +13,4 @@ const mapDispatchToProps = {
   fetchTopicConfig,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Settings)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(Settings);

+ 43 - 38
kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.spec.tsx

@@ -1,14 +1,20 @@
 import React from 'react';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import { screen } from '@testing-library/react';
-import Settings from 'components/Topics/Topic/Details/Settings/Settings';
+import Settings, {
+  Props,
+} from 'components/Topics/Topic/Details/Settings/Settings';
 import { TopicConfig } from 'generated-sources';
+import { clusterTopicSettingsPath } from 'lib/paths';
+import { getTopicStateFixtures } from 'redux/reducers/topics/__test__/fixtures';
 
 describe('Settings', () => {
+  const mockClusterName = 'Cluster_Name';
+  const mockTopicName = 'Topic_Name';
+
   let expectedResult: number;
   const mockFn = jest.fn();
-  const mockClusterName = 'Cluster Name';
-  const mockTopicName = 'Topic Name';
+
   const mockConfig: TopicConfig[] = [
     {
       name: 'first',
@@ -20,43 +26,50 @@ describe('Settings', () => {
     },
   ];
 
-  it('should check it returns null if no config is passed', () => {
-    render(
-      <Settings
-        clusterName={mockClusterName}
-        topicName={mockTopicName}
-        isFetched
-        fetchTopicConfig={mockFn}
-      />
+  const setUpComponent = (
+    props: Partial<Props> = {},
+    config?: TopicConfig[]
+  ) => {
+    const topic = {
+      name: mockTopicName,
+      config,
+    };
+    const topics = getTopicStateFixtures([topic]);
+
+    return render(
+      <WithRoute path={clusterTopicSettingsPath()}>
+        <Settings isFetched fetchTopicConfig={mockFn} {...props} />
+      </WithRoute>,
+      {
+        initialEntries: [
+          clusterTopicSettingsPath(mockClusterName, mockTopicName),
+        ],
+        preloadedState: {
+          topics,
+        },
+      }
     );
+  };
+
+  afterEach(() => {
+    mockFn.mockClear();
+  });
+
+  it('should check it returns null if no config is passed', () => {
+    setUpComponent();
 
     expect(screen.queryByRole('table')).not.toBeInTheDocument();
   });
 
   it('should show Page loader when it is in fetching state and config is given', () => {
-    render(
-      <Settings
-        clusterName={mockClusterName}
-        topicName={mockTopicName}
-        isFetched={false}
-        fetchTopicConfig={mockFn}
-        config={mockConfig}
-      />
-    );
+    setUpComponent({ isFetched: false }, mockConfig);
 
     expect(screen.queryByRole('table')).not.toBeInTheDocument();
     expect(screen.getByRole('progressbar')).toBeInTheDocument();
   });
 
   it('should check and return null if it is not fetched and config is not given', () => {
-    render(
-      <Settings
-        clusterName={mockClusterName}
-        topicName={mockTopicName}
-        isFetched={false}
-        fetchTopicConfig={mockFn}
-      />
-    );
+    setUpComponent({ isFetched: false });
 
     expect(screen.queryByRole('table')).not.toBeInTheDocument();
   });
@@ -64,15 +77,7 @@ describe('Settings', () => {
   describe('Settings Component with Data', () => {
     beforeEach(() => {
       expectedResult = mockConfig.length + 1; // include the header table row as well
-      render(
-        <Settings
-          clusterName={mockClusterName}
-          topicName={mockTopicName}
-          isFetched
-          fetchTopicConfig={mockFn}
-          config={mockConfig}
-        />
-      );
+      setUpComponent({ isFetched: true }, mockConfig);
     });
 
     it('should view the correct number of table row with header included elements after config fetching', () => {

+ 43 - 47
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx

@@ -3,36 +3,35 @@ import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import ClusterContext from 'components/contexts/ClusterContext';
 import Details from 'components/Topics/Topic/Details/Details';
-import { internalTopicPayload } from 'redux/reducers/topics/__test__/fixtures';
-import { render } from 'lib/testHelpers';
 import {
-  clusterTopicEditPath,
-  clusterTopicPath,
-  clusterTopicsPath,
-} from 'lib/paths';
-import { Router } from 'react-router-dom';
-import { createMemoryHistory } from 'history';
+  getTopicStateFixtures,
+  internalTopicPayload,
+} from 'redux/reducers/topics/__test__/fixtures';
+import { render, WithRoute } from 'lib/testHelpers';
+import { clusterTopicEditRelativePath, clusterTopicPath } from 'lib/paths';
+import { CleanUpPolicy, Topic } from 'generated-sources';
+
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+}));
 
 describe('Details', () => {
   const mockDelete = jest.fn();
   const mockClusterName = 'local';
   const mockClearTopicMessages = jest.fn();
-  const mockInternalTopicPayload = internalTopicPayload.internal;
   const mockRecreateTopic = jest.fn();
-  const defaultPathname = clusterTopicPath(
-    mockClusterName,
-    internalTopicPayload.name
-  );
-  const mockHistory = createMemoryHistory({
-    initialEntries: [defaultPathname],
-  });
-  jest.spyOn(mockHistory, 'push');
 
-  const setupComponent = (
-    pathname = defaultPathname,
-    history = mockHistory,
-    props = {}
-  ) =>
+  const topic: Topic = {
+    ...internalTopicPayload,
+    cleanUpPolicy: CleanUpPolicy.DELETE,
+    internal: false,
+  };
+
+  const mockTopicsState = getTopicStateFixtures([topic]);
+
+  const setupComponent = (props = {}) =>
     render(
       <ClusterContext.Provider
         value={{
@@ -42,24 +41,33 @@ describe('Details', () => {
           isTopicDeletionAllowed: true,
         }}
       >
-        <Router history={history}>
+        <WithRoute path={clusterTopicPath()}>
           <Details
-            clusterName={mockClusterName}
-            topicName={internalTopicPayload.name}
-            name={internalTopicPayload.name}
-            isInternal={false}
             deleteTopic={mockDelete}
             recreateTopic={mockRecreateTopic}
             clearTopicMessages={mockClearTopicMessages}
             isDeleted={false}
-            isDeletePolicy
             {...props}
           />
-        </Router>
+        </WithRoute>
       </ClusterContext.Provider>,
-      { pathname }
+      {
+        initialEntries: [
+          clusterTopicPath(mockClusterName, internalTopicPayload.name),
+        ],
+        preloadedState: {
+          topics: mockTopicsState,
+        },
+      }
     );
 
+  afterEach(() => {
+    mockNavigate.mockClear();
+    mockDelete.mockClear();
+    mockClearTopicMessages.mockClear();
+    mockRecreateTopic.mockClear();
+  });
+
   describe('when it has readonly flag', () => {
     it('does not render the Action button a Topic', () => {
       render(
@@ -72,15 +80,10 @@ describe('Details', () => {
           }}
         >
           <Details
-            clusterName={mockClusterName}
-            topicName={internalTopicPayload.name}
-            name={internalTopicPayload.name}
-            isInternal={mockInternalTopicPayload}
             deleteTopic={mockDelete}
             recreateTopic={mockRecreateTopic}
             clearTopicMessages={mockClearTopicMessages}
             isDeleted={false}
-            isDeletePolicy
           />
         </ClusterContext.Provider>
       );
@@ -148,30 +151,23 @@ describe('Details', () => {
       const button = screen.getAllByText('Edit settings')[0];
       userEvent.click(button);
 
-      const redirectRoute = clusterTopicEditPath(
-        mockClusterName,
-        internalTopicPayload.name
-      );
-
-      expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute);
+      expect(mockNavigate).toHaveBeenCalledWith(clusterTopicEditRelativePath);
     });
   });
 
   it('redirects to the correct route if topic is deleted', () => {
-    setupComponent(defaultPathname, mockHistory, { isDeleted: true });
-    const redirectRoute = clusterTopicsPath(mockClusterName);
+    setupComponent({ isDeleted: true });
 
-    expect(mockHistory.push).toHaveBeenCalledWith(redirectRoute);
+    expect(mockNavigate).toHaveBeenCalledWith('../..');
   });
 
   it('shows a confirmation popup on deleting topic messages', () => {
     setupComponent();
-    const { getByText } = screen;
-    const clearMessagesButton = getByText(/Clear messages/i);
+    const clearMessagesButton = screen.getAllByText(/Clear messages/i)[0];
     userEvent.click(clearMessagesButton);
 
     expect(
-      getByText(/Are you sure want to clear topic messages?/i)
+      screen.getByText(/Are you sure want to clear topic messages?/i)
     ).toBeInTheDocument();
   });
 

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

@@ -6,13 +6,13 @@ import { FormError } from 'components/common/Input/Input.styled';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import React from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
+import { RouteParamsClusterTopic } from 'lib/paths';
 import { ClusterName, TopicName } from 'redux/interfaces';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import * as S from './DangerZone.styled';
 
 export interface Props {
-  clusterName: string;
-  topicName: string;
   defaultPartitions: number;
   defaultReplicationFactor: number;
   partitionsCountIncreased: boolean;
@@ -30,8 +30,6 @@ export interface Props {
 }
 
 const DangerZone: React.FC<Props> = ({
-  clusterName,
-  topicName,
   defaultPartitions,
   defaultReplicationFactor,
   partitionsCountIncreased,
@@ -39,6 +37,8 @@ const DangerZone: React.FC<Props> = ({
   updateTopicPartitionsCount,
   updateTopicReplicationFactor,
 }) => {
+  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
+
   const [isPartitionsConfirmationVisible, setIsPartitionsConfirmationVisible] =
     React.useState<boolean>(false);
   const [

+ 3 - 19
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/DangerZoneContainer.ts

@@ -1,6 +1,5 @@
 import { connect } from 'react-redux';
-import { RootState, ClusterName, TopicName } from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RootState } from 'redux/interfaces';
 import {
   updateTopicPartitionsCount,
   updateTopicReplicationFactor,
@@ -12,11 +11,6 @@ import {
 
 import DangerZone from './DangerZone';
 
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
 type OwnProps = {
   defaultPartitions: number;
   defaultReplicationFactor: number;
@@ -24,16 +18,8 @@ type OwnProps = {
 
 const mapStateToProps = (
   state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-    defaultPartitions,
-    defaultReplicationFactor,
-  }: OwnProps & RouteComponentProps<RouteProps>
+  { defaultPartitions, defaultReplicationFactor }: OwnProps
 ) => ({
-  clusterName,
-  topicName,
   defaultPartitions,
   defaultReplicationFactor,
   partitionsCountIncreased: getTopicPartitionsCountIncreased(state),
@@ -45,6 +31,4 @@ const mapDispatchToProps = {
   updateTopicReplicationFactor,
 };
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(DangerZone)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(DangerZone);

+ 14 - 16
kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx

@@ -4,28 +4,30 @@ import DangerZone, {
 } from 'components/Topics/Topic/Edit/DangerZone/DangerZone';
 import { act, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { render } from 'lib/testHelpers';
+import { render, WithRoute } from 'lib/testHelpers';
 import {
   topicName,
   clusterName,
 } from 'components/Topics/Topic/Edit/__test__/fixtures';
+import { clusterTopicSendMessagePath } from 'lib/paths';
 
 const defaultPartitions = 3;
 const defaultReplicationFactor = 3;
 
 const renderComponent = (props?: Partial<Props>) =>
   render(
-    <DangerZone
-      clusterName={clusterName}
-      topicName={topicName}
-      defaultPartitions={defaultPartitions}
-      defaultReplicationFactor={defaultReplicationFactor}
-      partitionsCountIncreased={false}
-      replicationFactorUpdated={false}
-      updateTopicPartitionsCount={jest.fn()}
-      updateTopicReplicationFactor={jest.fn()}
-      {...props}
-    />
+    <WithRoute path={clusterTopicSendMessagePath()}>
+      <DangerZone
+        defaultPartitions={defaultPartitions}
+        defaultReplicationFactor={defaultReplicationFactor}
+        partitionsCountIncreased={false}
+        replicationFactorUpdated={false}
+        updateTopicPartitionsCount={jest.fn()}
+        updateTopicReplicationFactor={jest.fn()}
+        {...props}
+      />
+    </WithRoute>,
+    { initialEntries: [clusterTopicSendMessagePath(clusterName, topicName)] }
   );
 
 const clickOnDialogSubmitButton = () => {
@@ -199,8 +201,6 @@ describe('DangerZone', () => {
     await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
     rerender(
       <DangerZone
-        clusterName={clusterName}
-        topicName={topicName}
         defaultPartitions={defaultPartitions}
         defaultReplicationFactor={defaultReplicationFactor}
         partitionsCountIncreased
@@ -228,8 +228,6 @@ describe('DangerZone', () => {
     await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
     rerender(
       <DangerZone
-        clusterName={clusterName}
-        topicName={topicName}
         defaultPartitions={defaultPartitions}
         defaultReplicationFactor={defaultReplicationFactor}
         partitionsCountIncreased={false}

+ 13 - 17
kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx

@@ -9,20 +9,20 @@ import {
 } from 'redux/interfaces';
 import { useForm, FormProvider } from 'react-hook-form';
 import TopicForm from 'components/Topics/shared/Form/TopicForm';
-import { clusterTopicPath } from 'lib/paths';
-import { useHistory } from 'react-router-dom';
+import { RouteParamsClusterTopic } from 'lib/paths';
+import { useNavigate } from 'react-router-dom';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { topicFormValidationSchema } from 'lib/yupExtended';
 import { TOPIC_CUSTOM_PARAMS_PREFIX, TOPIC_CUSTOM_PARAMS } from 'lib/constants';
 import styled from 'styled-components';
 import PageHeading from 'components/common/PageHeading/PageHeading';
+import { useAppSelector } from 'lib/hooks/redux';
+import { getFullTopic } from 'redux/reducers/topics/selectors';
+import useAppParams from 'lib/hooks/useAppParams';
 
 import DangerZoneContainer from './DangerZone/DangerZoneContainer';
 
 export interface Props {
-  clusterName: ClusterName;
-  topicName: TopicName;
-  topic?: TopicWithDetailedInfo;
   isFetched: boolean;
   isTopicUpdated: boolean;
   fetchTopicConfig: (payload: {
@@ -34,11 +34,6 @@ export interface Props {
     topicName: TopicName;
     form: TopicFormDataRaw;
   }) => void;
-  updateTopicPartitionsCount: (payload: {
-    clusterName: string;
-    topicname: string;
-    partitions: number;
-  }) => void;
 }
 
 const EditWrapperStyled = styled.div`
@@ -83,22 +78,24 @@ const topicParams = (topic: TopicWithDetailedInfo | undefined) => {
 let formInit = false;
 
 const Edit: React.FC<Props> = ({
-  clusterName,
-  topicName,
-  topic,
   isFetched,
   isTopicUpdated,
   fetchTopicConfig,
   updateTopic,
 }) => {
+  const { clusterName, topicName } = useAppParams<RouteParamsClusterTopic>();
+
+  const topic = useAppSelector((state) => getFullTopic(state, topicName));
+
   const defaultValues = React.useMemo(() => topicParams(topic), [topic]);
+
   const methods = useForm<TopicFormData>({
     defaultValues,
     resolver: yupResolver(topicFormValidationSchema),
   });
 
   const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
-  const history = useHistory();
+  const navigate = useNavigate();
 
   React.useEffect(() => {
     fetchTopicConfig({ clusterName, topicName });
@@ -106,10 +103,9 @@ const Edit: React.FC<Props> = ({
 
   React.useEffect(() => {
     if (isSubmitting && isTopicUpdated) {
-      const { name } = methods.getValues();
-      history.push(clusterTopicPath(clusterName, name));
+      navigate('../');
     }
-  }, [isSubmitting, isTopicUpdated, clusterName, methods, history]);
+  }, [isSubmitting, isTopicUpdated, clusterName, navigate]);
 
   if (!isFetched || !topic || !topic.config) {
     return null;

+ 3 - 22
kafka-ui-react-app/src/components/Topics/Topic/Edit/EditContainer.tsx

@@ -1,6 +1,5 @@
 import { connect } from 'react-redux';
-import { RootState, ClusterName, TopicName } from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RootState } from 'redux/interfaces';
 import {
   updateTopic,
   fetchTopicConfig,
@@ -8,29 +7,11 @@ import {
 import {
   getTopicConfigFetched,
   getTopicUpdated,
-  getFullTopic,
 } from 'redux/reducers/topics/selectors';
 
 import Edit from './Edit';
 
-interface RouteProps {
-  clusterName: ClusterName;
-  topicName: TopicName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { topicName, clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  topicName,
-  topic: getFullTopic(state, topicName),
+const mapStateToProps = (state: RootState) => ({
   isFetched: getTopicConfigFetched(state),
   isTopicUpdated: getTopicUpdated(state),
 });
@@ -40,4 +21,4 @@ const mapDispatchToProps = {
   updateTopic,
 };
 
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));
+export default connect(mapStateToProps, mapDispatchToProps)(Edit);

部分文件因文件數量過多而無法顯示