소스 검색

Redesign (#1045)

* Refactor topic creation

* Remove unused thunk

* Remove excess interface

* Add New page snapshot test

* Refactor new component tests

* Remove excess function

* Add typography variables and classes

* Add font families

* Implement custom button

* Get rid of enums

* Add theme

* Separate styles from logic

* Feature/layout redesign (#862)

* Refactor pages general layout

* Refactor breadcrumbs

* Refactor brokers metrics

* Fix toggle position

Co-authored-by: azat.belgibayev <azat.belgibayev@almatech.dev>

* add redesigned new menu item

* remove styles from theme

* update tests

* fix local and app wide styles

* add tests

* Add theme

* Add types to the styles

* update menu item prop prefixes, minor fixes

* add theme styles, move interface, update test, snapshot

* add optional styling

* add isActive props, propagate component, update tests

* remove button

* Revert "remove button"

This reverts commit 4a9c87d8d88f62f99da2486a991a976116a20db7.

* add tests for styled button

* remove ternary operator from style

* import styled from lib/

* Custom Inputs  (#890)

* Implement and test custom input

* Custom select (#896)

* Implement custom select

* Fix Metrics component (#914)

* Add styled table header cell component (#901)

* Redesign menu (#918)

* Finish styling menu

* Styled Table

* Fix styled table

* Allow custom buttons work as links

* Restyle Breadcrumb

* Topics list (#946)

* Redesign pagination

* Fix styled components usage

* Topic messages (#959)

* Topic Consumer Groups

* Message settings

* Finish styling indicators

* Style the dashboard

* Finish with the topics page

* Style consumer groups list

* Restyle the consumer group details

* Style alerts

* Style confirmation modal

* Update DangerZone.spec.tsx

* redesign schema registry

* Add Topic details snapshot

* Style Page Loader

* Style connectors list

* Style KSQL

* Remove all the classes from the styled components (#1049)

* Redesign topic form (#1051)

* Redesign connect details (#1053)

* Update types for styled-components (#1054)

* Redesign some minor forms in the app (#1062)

* Fix alert styles

* Get rid of bulma/layout styles

* Fix form styling

* Custom Switch component

* fix border-radius property of metrick widgets

* get rid of warnings in tests

* use jest-styled-components

* cleanup

* get rid of some bulma modules

* refactor metrics component

* get rid of JSON-tree. Json Editor redesign

* update proxy config

* Refactor Alerts component (#1124)

* Refactor tests (#1129)

* App layout update (#1127)

* ‘App-layout-update’

* toBeNull changed to toBeInDocument

* scss file removed

* App navbar layout update

* navbar test

* code smells local refactoring

* StyledMenuItem code smells refactoring

* StyledClusterTab code smells refactoring

* ConfirmationModalWrapper code smells refactoring

* input icon and label code smells refactoring

* navburger displaying fixed

* Get rid of classes

* fix code smells

* refactor styles

* refactor styles

Co-authored-by: Oleg Shuralev <workshur@gmail.com>

* Refactored Cluster nav (#1147)

* Update caniuse

* refactor Nav component

* Update sonars config

* refactor Nav component + specs

* Specs

* Feature/code smells removing (#1148)

* StyledSelect code smell refactoring

* SecondaryTabs code smell refactoring

* TextareaStyled code smell refactoring

* TableStyled code smell refactoring

* StyledTableHeaderCell code smell refactoring

* Add custom render with theme provider wrapper for testing lib (#1152)

* Added cleanupPolicy to topic details. Closes #999 (#1067)

* Rename "latest first" to "oldest first"

* Switch to redux-toolkit. Refactoring (#1171)

* Switch to redux-toolkit

* Fix #1207 (cherry-pick)

* refactor metrics (#1253)

Co-authored-by: Azat Belgibayev <belg.azat@gmail.com>
Co-authored-by: Alexander <mr.afigitelniychuvak@gmail.com>
Co-authored-by: azat.belgibayev <azat.belgibayev@almatech.dev>
Co-authored-by: sergei <scheremnov@provectus.com>
Co-authored-by: Alexander Krivonosov <31561808+GneyHabub@users.noreply.github.com>
Co-authored-by: Alina Miryuk <alinamiryuk@mail.ru>
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
Oleg Shur 3 년 전
부모
커밋
7e5e8d9268
100개의 변경된 파일4718개의 추가작업 그리고 4788개의 파일을 삭제
  1. 12 7
      kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
  2. 3 1
      kafka-ui-react-app/.eslintrc.json
  3. 256 144
      kafka-ui-react-app/package-lock.json
  4. 9 3
      kafka-ui-react-app/package.json
  5. 1 1
      kafka-ui-react-app/sonar-project.properties
  6. 0 48
      kafka-ui-react-app/src/components/Alert/Alert.tsx
  7. 0 119
      kafka-ui-react-app/src/components/Alert/__tests__/Alert.spec.tsx
  8. 0 35
      kafka-ui-react-app/src/components/Alert/__tests__/__snapshots__/Alert.spec.tsx.snap
  9. 27 0
      kafka-ui-react-app/src/components/Alerts/Alert.styled.ts
  10. 28 0
      kafka-ui-react-app/src/components/Alerts/Alert.tsx
  11. 44 0
      kafka-ui-react-app/src/components/Alerts/Alerts.tsx
  12. 36 0
      kafka-ui-react-app/src/components/Alerts/__tests__/Alert.spec.tsx
  13. 59 0
      kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx
  14. 0 98
      kafka-ui-react-app/src/components/App.scss
  15. 177 0
      kafka-ui-react-app/src/components/App.styled.ts
  16. 74 89
      kafka-ui-react-app/src/components/App.tsx
  17. 0 21
      kafka-ui-react-app/src/components/AppContainer.tsx
  18. 87 83
      kafka-ui-react-app/src/components/Brokers/Brokers.tsx
  19. 0 38
      kafka-ui-react-app/src/components/Brokers/BrokersContainer.ts
  20. 41 77
      kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx
  21. 0 8
      kafka-ui-react-app/src/components/Brokers/__test__/BrokersContainer.spec.tsx
  22. 0 779
      kafka-ui-react-app/src/components/Brokers/__test__/__snapshots__/Brokers.spec.tsx.snap
  23. 5 8
      kafka-ui-react-app/src/components/Cluster/Cluster.tsx
  24. 81 52
      kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx
  25. 0 71
      kafka-ui-react-app/src/components/Connect/Breadcrumbs/Breadcrumbs.tsx
  26. 0 65
      kafka-ui-react-app/src/components/Connect/Breadcrumbs/__tests__/Breadcrumbs.spec.tsx
  27. 0 109
      kafka-ui-react-app/src/components/Connect/Breadcrumbs/__tests__/__snapshots__/Breadcrumbs.spec.tsx.snap
  28. 1 16
      kafka-ui-react-app/src/components/Connect/Connect.tsx
  29. 0 21
      kafka-ui-react-app/src/components/Connect/ConnectorStatusTag.tsx
  30. 47 42
      kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
  31. 19 15
      kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx
  32. 753 120
      kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/__snapshots__/Actions.spec.tsx.snap
  33. 17 7
      kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx
  34. 17 7
      kafka-ui-react-app/src/components/Connect/Details/Config/__test__/__snapshots__/Config.spec.tsx.snap
  35. 41 44
      kafka-ui-react-app/src/components/Connect/Details/Details.tsx
  36. 26 37
      kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx
  37. 11 9
      kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx
  38. 215 57
      kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/__snapshots__/Overview.spec.tsx.snap
  39. 15 18
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx
  40. 18 13
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx
  41. 110 20
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/__snapshots__/ListItem.spec.tsx.snap
  42. 9 9
      kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx
  43. 15 11
      kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx
  44. 200 26
      kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/__snapshots__/Tasks.spec.tsx.snap
  45. 18 14
      kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx
  46. 104 36
      kafka-ui-react-app/src/components/Connect/Details/__tests__/__snapshots__/Details.spec.tsx.snap
  47. 20 0
      kafka-ui-react-app/src/components/Connect/Edit/Edit.styled.ts
  48. 32 31
      kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx
  49. 16 12
      kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx
  50. 173 71
      kafka-ui-react-app/src/components/Connect/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap
  51. 46 41
      kafka-ui-react-app/src/components/Connect/List/List.tsx
  52. 21 26
      kafka-ui-react-app/src/components/Connect/List/ListItem.tsx
  53. 30 26
      kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx
  54. 18 19
      kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
  55. 476 215
      kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap
  56. 72 60
      kafka-ui-react-app/src/components/Connect/New/New.tsx
  57. 14 10
      kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx
  58. 268 43
      kafka-ui-react-app/src/components/Connect/New/__tests__/__snapshots__/New.spec.tsx.snap
  59. 0 20
      kafka-ui-react-app/src/components/Connect/StatusTag.tsx
  60. 0 38
      kafka-ui-react-app/src/components/Connect/__tests__/StatusTag.spec.tsx
  61. 2 14
      kafka-ui-react-app/src/components/Connect/__tests__/__snapshots__/Connect.spec.tsx.snap
  62. 0 33
      kafka-ui-react-app/src/components/Connect/__tests__/__snapshots__/StatusTag.spec.tsx.snap
  63. 18 21
      kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx
  64. 0 32
      kafka-ui-react-app/src/components/ConsumerGroups/ConsumersGroupsContainer.ts
  65. 101 107
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  66. 0 49
      kafka-ui-react-app/src/components/ConsumerGroups/Details/DetailsContainer.ts
  67. 6 0
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.styled.ts
  68. 24 21
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx
  69. 69 0
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.styled.ts
  70. 141 178
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx
  71. 0 47
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsetsContainer.ts
  72. 130 146
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx
  73. 0 184
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/__snapshots__/ResetOffsets.spec.tsx.snap
  74. 15 0
      kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContent.styled.ts
  75. 47 0
      kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx
  76. 99 86
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx
  77. 0 8
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/DetailsContainer.spec.tsx
  78. 0 157
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/__snapshots__/Details.spec.tsx.snap
  79. 49 61
      kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx
  80. 0 26
      kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.ts
  81. 10 11
      kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx
  82. 36 36
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx
  83. 0 8
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListContainer.spec.tsx
  84. 16 3
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx
  85. 0 78
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx
  86. 68 33
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx
  87. 1 1
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts
  88. 0 87
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx
  89. 19 23
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx
  90. 10 2
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidgetContainer.spec.tsx
  91. 0 171
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/__snapshots__/ClusterWidget.spec.tsx.snap
  92. 1 8
      kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx
  93. 0 4
      kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx
  94. 0 30
      kafka-ui-react-app/src/components/KsqlDb/BreadCrumbs/BreadCrumbs.tsx
  95. 0 33
      kafka-ui-react-app/src/components/KsqlDb/BreadCrumbs/__test__/BreadCrumbs.spec.tsx
  96. 4 10
      kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx
  97. 30 28
      kafka-ui-react-app/src/components/KsqlDb/List/List.tsx
  98. 5 7
      kafka-ui-react-app/src/components/KsqlDb/List/ListItem.tsx
  99. 29 55
      kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx
  100. 26 0
      kafka-ui-react-app/src/components/KsqlDb/Query/Query.styled.ts

+ 12 - 7
kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml

@@ -1699,12 +1699,7 @@ components:
         underReplicatedPartitions:
           type: integer
         cleanUpPolicy:
-          type: string
-          enum:
-            - DELETE
-            - COMPACT
-            - COMPACT_DELETE
-            - UNKNOWN
+          $ref: '#/components/schemas/CleanUpPolicy'
         partitions:
           type: array
           items:
@@ -1752,6 +1747,8 @@ components:
           type: integer
         underReplicatedPartitions:
           type: integer
+        cleanUpPolicy:
+          $ref: '#/components/schemas/CleanUpPolicy'
       required:
         - name
 
@@ -2630,4 +2627,12 @@ components:
         value:
           type: string
         source:
-          $ref: '#/components/schemas/ConfigSource'
+          $ref: '#/components/schemas/ConfigSource'
+
+    CleanUpPolicy:
+      type: string
+      enum:
+        - DELETE
+        - COMPACT
+        - COMPACT_DELETE
+        - UNKNOWN

+ 3 - 1
kafka-ui-react-app/.eslintrc.json

@@ -24,6 +24,7 @@
   "extends": [
     "airbnb-typescript",
     "plugin:@typescript-eslint/recommended",
+    "plugin:jest-dom/recommended",
     "plugin:prettier/recommended",
     "prettier"
   ],
@@ -41,7 +42,8 @@
     }],
     "import/no-relative-parent-imports": "error",
     "no-debugger": "warn",
-    "react/jsx-props-no-spreading": "off"
+    "react/jsx-props-no-spreading": "off",
+    "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state"] }]
   },
   "overrides": [
     {

+ 256 - 144
kafka-ui-react-app/package-lock.json

@@ -15,14 +15,12 @@
     "@babel/compat-data": {
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz",
-      "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==",
-      "dev": true
+      "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w=="
     },
     "@babel/core": {
       "version": "7.14.6",
       "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.6.tgz",
       "integrity": "sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA==",
-      "dev": true,
       "requires": {
         "@babel/code-frame": "^7.14.5",
         "@babel/generator": "^7.14.5",
@@ -45,7 +43,6 @@
           "version": "4.3.1",
           "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
           "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-          "dev": true,
           "requires": {
             "ms": "2.1.2"
           }
@@ -54,7 +51,6 @@
           "version": "2.2.0",
           "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
           "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
-          "dev": true,
           "requires": {
             "minimist": "^1.2.5"
           }
@@ -62,20 +58,17 @@
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-          "dev": true
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
         },
         "semver": {
           "version": "6.3.0",
           "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
         },
         "source-map": {
           "version": "0.5.7",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-          "dev": true
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
         }
       }
     },
@@ -83,7 +76,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz",
       "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5",
         "jsesc": "^2.5.1",
@@ -93,8 +85,7 @@
         "source-map": {
           "version": "0.5.7",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-          "dev": true
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
         }
       }
     },
@@ -102,7 +93,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz",
       "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -121,7 +111,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz",
       "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==",
-      "dev": true,
       "requires": {
         "@babel/compat-data": "^7.14.5",
         "@babel/helper-validator-option": "^7.14.5",
@@ -132,8 +121,7 @@
         "semver": {
           "version": "6.3.0",
           "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-          "dev": true
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
         }
       }
     },
@@ -213,7 +201,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz",
       "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==",
-      "dev": true,
       "requires": {
         "@babel/helper-get-function-arity": "^7.14.5",
         "@babel/template": "^7.14.5",
@@ -224,7 +211,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz",
       "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -233,7 +219,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz",
       "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -242,7 +227,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz",
       "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -251,7 +235,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz",
       "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -260,7 +243,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz",
       "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==",
-      "dev": true,
       "requires": {
         "@babel/helper-module-imports": "^7.14.5",
         "@babel/helper-replace-supers": "^7.14.5",
@@ -276,7 +258,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz",
       "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -302,7 +283,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz",
       "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==",
-      "dev": true,
       "requires": {
         "@babel/helper-member-expression-to-functions": "^7.14.5",
         "@babel/helper-optimise-call-expression": "^7.14.5",
@@ -314,7 +294,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz",
       "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -332,7 +311,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz",
       "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==",
-      "dev": true,
       "requires": {
         "@babel/types": "^7.14.5"
       }
@@ -345,8 +323,7 @@
     "@babel/helper-validator-option": {
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz",
-      "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==",
-      "dev": true
+      "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow=="
     },
     "@babel/helper-wrap-function": {
       "version": "7.14.5",
@@ -364,7 +341,6 @@
       "version": "7.14.6",
       "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz",
       "integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==",
-      "dev": true,
       "requires": {
         "@babel/template": "^7.14.5",
         "@babel/traverse": "^7.14.5",
@@ -417,8 +393,7 @@
     "@babel/parser": {
       "version": "7.14.6",
       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz",
-      "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==",
-      "dev": true
+      "integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ=="
     },
     "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
       "version": "7.14.5",
@@ -1349,7 +1324,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz",
       "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==",
-      "dev": true,
       "requires": {
         "@babel/code-frame": "^7.14.5",
         "@babel/parser": "^7.14.5",
@@ -1360,7 +1334,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz",
       "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==",
-      "dev": true,
       "requires": {
         "@babel/code-frame": "^7.14.5",
         "@babel/generator": "^7.14.5",
@@ -1377,7 +1350,6 @@
           "version": "4.3.1",
           "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
           "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-          "dev": true,
           "requires": {
             "ms": "2.1.2"
           }
@@ -1385,14 +1357,12 @@
         "globals": {
           "version": "11.12.0",
           "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
-          "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
-          "dev": true
+          "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
         },
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-          "dev": true
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
         }
       }
     },
@@ -1400,7 +1370,6 @@
       "version": "7.14.5",
       "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz",
       "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==",
-      "dev": true,
       "requires": {
         "@babel/helper-validator-identifier": "^7.14.5",
         "to-fast-properties": "^2.0.0"
@@ -1449,6 +1418,29 @@
       "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==",
       "dev": true
     },
+    "@emotion/is-prop-valid": {
+      "version": "0.8.8",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+      "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+      "requires": {
+        "@emotion/memoize": "0.7.4"
+      }
+    },
+    "@emotion/memoize": {
+      "version": "0.7.4",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+      "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
+    },
+    "@emotion/stylis": {
+      "version": "0.8.5",
+      "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
+      "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
+    },
+    "@emotion/unitless": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+      "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+    },
     "@eslint/eslintrc": {
       "version": "0.4.3",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
@@ -2144,6 +2136,7 @@
       "version": "27.4.2",
       "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz",
       "integrity": "sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==",
+      "dev": true,
       "requires": {
         "@types/istanbul-lib-coverage": "^2.0.0",
         "@types/istanbul-reports": "^3.0.0",
@@ -2156,6 +2149,7 @@
           "version": "16.0.4",
           "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
           "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
+          "dev": true,
           "requires": {
             "@types/yargs-parser": "*"
           }
@@ -2348,6 +2342,24 @@
       "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz",
       "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q=="
     },
+    "@reduxjs/toolkit": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.2.tgz",
+      "integrity": "sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==",
+      "requires": {
+        "immer": "^9.0.6",
+        "redux": "^4.1.0",
+        "redux-thunk": "^2.3.0",
+        "reselect": "^4.0.0"
+      },
+      "dependencies": {
+        "immer": {
+          "version": "9.0.7",
+          "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz",
+          "integrity": "sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA=="
+        }
+      }
+    },
     "@rollup/plugin-node-resolve": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz",
@@ -2569,6 +2581,26 @@
         "pretty-format": "^27.0.2"
       },
       "dependencies": {
+        "@jest/types": {
+          "version": "27.2.5",
+          "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz",
+          "integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==",
+          "requires": {
+            "@types/istanbul-lib-coverage": "^2.0.0",
+            "@types/istanbul-reports": "^3.0.0",
+            "@types/node": "*",
+            "@types/yargs": "^16.0.0",
+            "chalk": "^4.0.0"
+          }
+        },
+        "@types/yargs": {
+          "version": "16.0.4",
+          "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
+          "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
+          "requires": {
+            "@types/yargs-parser": "*"
+          }
+        },
         "ansi-styles": {
           "version": "5.2.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
@@ -2593,6 +2625,20 @@
             "ansi-regex": "^5.0.1",
             "ansi-styles": "^5.0.0",
             "react-is": "^17.0.1"
+          },
+          "dependencies": {
+            "@jest/types": {
+              "version": "27.4.2",
+              "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz",
+              "integrity": "sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==",
+              "requires": {
+                "@types/istanbul-lib-coverage": "^2.0.0",
+                "@types/istanbul-reports": "^3.0.0",
+                "@types/node": "*",
+                "@types/yargs": "^16.0.0",
+                "chalk": "^4.0.0"
+              }
+            }
           }
         },
         "react-is": {
@@ -2640,6 +2686,15 @@
         "@testing-library/dom": "^8.0.0"
       }
     },
+    "@testing-library/user-event": {
+      "version": "13.5.0",
+      "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
+      "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.12.5"
+      }
+    },
     "@tootallnate/once": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -2716,11 +2771,6 @@
         "@babel/types": "^7.3.0"
       }
     },
-    "@types/base16": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@types/base16/-/base16-1.0.2.tgz",
-      "integrity": "sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg=="
-    },
     "@types/cheerio": {
       "version": "0.22.29",
       "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.29.tgz",
@@ -2934,15 +2984,8 @@
     "@types/lodash": {
       "version": "4.14.177",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz",
-      "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw=="
-    },
-    "@types/lodash.curry": {
-      "version": "4.1.6",
-      "resolved": "https://registry.npmjs.org/@types/lodash.curry/-/lodash.curry-4.1.6.tgz",
-      "integrity": "sha512-x3ctCcmOYqRrihNNnQJW6fe/yZFCgnrIa6p80AiPQRO8Jis29bBdy1dEw1FwngoF/mCZa3Bx+33fUZvOEE635Q==",
-      "requires": {
-        "@types/lodash": "*"
-      }
+      "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==",
+      "dev": true
     },
     "@types/minimatch": {
       "version": "3.0.4",
@@ -3112,6 +3155,16 @@
       "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
       "dev": true
     },
+    "@types/styled-components": {
+      "version": "5.1.14",
+      "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.14.tgz",
+      "integrity": "sha512-d6P1/tyNytqKwam3cQXq7a9uPtovc/mdAs7dBiz1YbDdNIT3X4WmuFU78YdSYh84TXVuhOwezZ3EeKuNBhwsHQ==",
+      "requires": {
+        "@types/hoist-non-react-statics": "*",
+        "@types/react": "*",
+        "csstype": "^3.0.2"
+      }
+    },
     "@types/tapable": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz",
@@ -4491,6 +4544,22 @@
         "@babel/helper-define-polyfill-provider": "^0.2.2"
       }
     },
+    "babel-plugin-styled-components": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz",
+      "integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==",
+      "requires": {
+        "@babel/helper-annotate-as-pure": "^7.0.0",
+        "@babel/helper-module-imports": "^7.0.0",
+        "babel-plugin-syntax-jsx": "^6.18.0",
+        "lodash": "^4.17.11"
+      }
+    },
+    "babel-plugin-syntax-jsx": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
+      "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
+    },
     "babel-plugin-syntax-object-rest-spread": {
       "version": "6.13.0",
       "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
@@ -4860,11 +4929,6 @@
         }
       }
     },
-    "base16": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
-      "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
-    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5091,7 +5155,6 @@
       "version": "4.16.6",
       "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
       "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
-      "dev": true,
       "requires": {
         "caniuse-lite": "^1.0.30001219",
         "colorette": "^1.2.2",
@@ -5163,11 +5226,6 @@
       "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz",
       "integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g=="
     },
-    "bulma-switch": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz",
-      "integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA=="
-    },
     "bytes": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@@ -5289,6 +5347,11 @@
         }
       }
     },
+    "camelize": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+      "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+    },
     "caniuse-api": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -5302,10 +5365,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30001237",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
-      "integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==",
-      "dev": true
+      "version": "1.0.30001283",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz",
+      "integrity": "sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg=="
     },
     "capture-exit": {
       "version": "2.0.0",
@@ -5628,6 +5690,7 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
       "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
+      "dev": true,
       "requires": {
         "color-convert": "^1.9.1",
         "color-string": "^1.5.4"
@@ -5650,6 +5713,7 @@
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
       "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
+      "dev": true,
       "requires": {
         "color-name": "^1.0.0",
         "simple-swizzle": "^0.2.2"
@@ -5658,8 +5722,7 @@
     "colorette": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
-      "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
-      "dev": true
+      "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
     },
     "combined-stream": {
       "version": "1.0.8",
@@ -5898,7 +5961,6 @@
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
       "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
-      "dev": true,
       "requires": {
         "safe-buffer": "~5.1.1"
       },
@@ -5906,8 +5968,7 @@
         "safe-buffer": {
           "version": "5.1.2",
           "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-          "dev": true
+          "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
         }
       }
     },
@@ -5957,8 +6018,7 @@
     "core-js": {
       "version": "3.14.0",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.14.0.tgz",
-      "integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==",
-      "dev": true
+      "integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA=="
     },
     "core-js-compat": {
       "version": "3.14.0",
@@ -6110,6 +6170,11 @@
         "postcss": "^7.0.5"
       }
     },
+    "css-color-keywords": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+      "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
+    },
     "css-color-names": {
       "version": "0.0.4",
       "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
@@ -6220,6 +6285,16 @@
       "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
       "dev": true
     },
+    "css-to-react-native": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
+      "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
+      "requires": {
+        "camelize": "^1.0.0",
+        "css-color-keywords": "^1.0.0",
+        "postcss-value-parser": "^4.0.2"
+      }
+    },
     "css-tree": {
       "version": "1.0.0-alpha.37",
       "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -7037,8 +7112,7 @@
     "electron-to-chromium": {
       "version": "1.3.752",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz",
-      "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==",
-      "dev": true
+      "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A=="
     },
     "elliptic": {
       "version": "6.5.4",
@@ -7347,8 +7421,7 @@
     "escalade": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
-      "dev": true
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
     },
     "escape-html": {
       "version": "1.0.3",
@@ -7880,6 +7953,35 @@
         "@typescript-eslint/experimental-utils": "^4.0.1"
       }
     },
+    "eslint-plugin-jest-dom": {
+      "version": "3.9.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.9.2.tgz",
+      "integrity": "sha512-DKNW6nxYkBvwv36WcYFxapCalGjOGSWUu5PREpDVuXGbEns3S5jhr+mZ5W2N6MxbOWw/2U61C1JVLH31gwVjOQ==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@testing-library/dom": "^7.28.1",
+        "requireindex": "^1.2.0"
+      },
+      "dependencies": {
+        "@testing-library/dom": {
+          "version": "7.31.2",
+          "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz",
+          "integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.10.4",
+            "@babel/runtime": "^7.12.5",
+            "@types/aria-query": "^4.2.0",
+            "aria-query": "^4.2.2",
+            "chalk": "^4.1.0",
+            "dom-accessibility-api": "^0.5.6",
+            "lz-string": "^1.4.4",
+            "pretty-format": "^26.6.2"
+          }
+        }
+      }
+    },
     "eslint-plugin-jsx-a11y": {
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz",
@@ -8693,7 +8795,6 @@
       "version": "9.11.0",
       "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz",
       "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==",
-      "dev": true,
       "requires": {
         "@babel/core": "^7.0.0",
         "@babel/runtime": "^7.0.0",
@@ -8708,10 +8809,9 @@
       },
       "dependencies": {
         "debug": {
-          "version": "4.3.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
-          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
-          "dev": true,
+          "version": "4.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+          "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
           "requires": {
             "ms": "2.1.2"
           }
@@ -8719,14 +8819,12 @@
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-          "dev": true
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
         },
         "path-to-regexp": {
           "version": "2.4.0",
           "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz",
-          "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==",
-          "dev": true
+          "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w=="
         }
       }
     },
@@ -9351,8 +9449,7 @@
     "gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
-      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
-      "dev": true
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
     },
     "get-caller-file": {
       "version": "2.0.5",
@@ -9432,8 +9529,7 @@
     "glob-to-regexp": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
-      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
-      "dev": true
+      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
     },
     "global-modules": {
       "version": "2.0.0",
@@ -10310,7 +10406,8 @@
     "is-arrayish": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+      "dev": true
     },
     "is-bigint": {
       "version": "1.0.2",
@@ -10595,8 +10692,7 @@
     "is-subset": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
-      "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
-      "dev": true
+      "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY="
     },
     "is-symbol": {
       "version": "1.0.4",
@@ -12105,6 +12201,15 @@
         "xml": "^1.0.1"
       }
     },
+    "jest-styled-components": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/jest-styled-components/-/jest-styled-components-7.0.6.tgz",
+      "integrity": "sha512-asCitkJfaO1TX60nbIBXDUglblDFvKl9aKA5IV09AmO2zL6DC89MgrK73/e7zXzMOpzyVmuXQLLYlsDUSYIb7g==",
+      "dev": true,
+      "requires": {
+        "css": "^3.0.0"
+      }
+    },
     "jest-util": {
       "version": "26.6.2",
       "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz",
@@ -12328,8 +12433,7 @@
     "jsesc": {
       "version": "2.5.2",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
-      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
-      "dev": true
+      "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
     },
     "json-parse-better-errors": {
       "version": "1.0.2",
@@ -12670,11 +12774,6 @@
       "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
       "dev": true
     },
-    "lodash.curry": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
-      "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA="
-    },
     "lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -12724,8 +12823,7 @@
     "lodash.sortby": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
-      "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
-      "dev": true
+      "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
     },
     "lodash.template": {
       "version": "4.5.0",
@@ -13565,8 +13663,7 @@
     "node-releases": {
       "version": "1.1.73",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
-      "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
-      "dev": true
+      "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg=="
     },
     "normalize-package-data": {
       "version": "2.5.0",
@@ -15416,8 +15513,7 @@
     "postcss-value-parser": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
-      "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
-      "dev": true
+      "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
     },
     "postcss-values-parser": {
       "version": "2.0.1",
@@ -15673,8 +15769,7 @@
     "querystring": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz",
-      "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==",
-      "dev": true
+      "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg=="
     },
     "querystring-es3": {
       "version": "0.2.1",
@@ -15789,19 +15884,6 @@
         "whatwg-fetch": "^3.4.1"
       }
     },
-    "react-base16-styling": {
-      "version": "0.8.0",
-      "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.8.0.tgz",
-      "integrity": "sha512-ElvciPaL4xpWh7ISX7ugkNS/dvoh7DpVMp4t93ngnEsS2LkMd8Gu+cDDOLis2rj4889CNK662UdjOfv3wvZg9w==",
-      "requires": {
-        "@types/base16": "^1.0.2",
-        "@types/lodash.curry": "^4.1.6",
-        "base16": "^1.0.0",
-        "color": "^3.1.2",
-        "csstype": "^3.0.2",
-        "lodash.curry": "^4.1.1"
-      }
-    },
     "react-datepicker": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.5.0.tgz",
@@ -16065,16 +16147,6 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
-    "react-json-tree": {
-      "version": "0.15.0",
-      "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.15.0.tgz",
-      "integrity": "sha512-/bEFXZBfLFiep6ReuzatR8mz9G7sRmejElRDgcAuqY0Jsx7llouax2DM03rlQifrUJgmvTGmPA+olyWYyGagqA==",
-      "requires": {
-        "@types/prop-types": "^15.7.3",
-        "prop-types": "^15.7.2",
-        "react-base16-styling": "^0.8.0"
-      }
-    },
     "react-multi-select-component": {
       "version": "4.0.6",
       "resolved": "https://registry.npmjs.org/react-multi-select-component/-/react-multi-select-component-4.0.6.tgz",
@@ -16443,9 +16515,9 @@
       }
     },
     "redux-thunk": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
-      "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz",
+      "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q=="
     },
     "reflect-metadata": {
       "version": "0.1.13",
@@ -16622,6 +16694,12 @@
       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
       "dev": true
     },
+    "requireindex": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+      "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+      "dev": true
+    },
     "requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -17407,6 +17485,11 @@
         "safe-buffer": "^5.0.1"
       }
     },
+    "shallowequal": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+      "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+    },
     "shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -17456,6 +17539,7 @@
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
       "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
+      "dev": true,
       "requires": {
         "is-arrayish": "^0.3.1"
       }
@@ -18178,6 +18262,38 @@
         "schema-utils": "^2.7.0"
       }
     },
+    "styled-components": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.1.tgz",
+      "integrity": "sha512-JThv2JRzyH0NOIURrk9iskdxMSAAtCfj/b2Sf1WJaCUsloQkblepy1jaCLX/bYE+mhYo3unmwVSI9I5d9ncSiQ==",
+      "requires": {
+        "@babel/helper-module-imports": "^7.0.0",
+        "@babel/traverse": "^7.4.5",
+        "@emotion/is-prop-valid": "^0.8.8",
+        "@emotion/stylis": "^0.8.4",
+        "@emotion/unitless": "^0.7.4",
+        "babel-plugin-styled-components": ">= 1.12.0",
+        "css-to-react-native": "^3.0.0",
+        "hoist-non-react-statics": "^3.0.0",
+        "shallowequal": "^1.1.0",
+        "supports-color": "^5.5.0"
+      },
+      "dependencies": {
+        "has-flag": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
     "stylehacks": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
@@ -18769,8 +18885,7 @@
     "to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
-      "dev": true
+      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
     },
     "to-object-path": {
       "version": "0.3.0",
@@ -18827,7 +18942,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
       "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
-      "dev": true,
       "requires": {
         "punycode": "^2.1.0"
       }
@@ -19733,8 +19847,7 @@
     "webidl-conversions": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
-      "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
-      "dev": true
+      "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
     },
     "webpack": {
       "version": "4.44.2",
@@ -20774,7 +20887,6 @@
       "version": "6.5.0",
       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
       "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
-      "dev": true,
       "requires": {
         "lodash.sortby": "^4.7.0",
         "tr46": "^1.0.1",

+ 9 - 3
kafka-ui-react-app/package.json

@@ -7,18 +7,20 @@
     "@fortawesome/fontawesome-free": "^5.15.4",
     "@hookform/error-message": "^2.0.0",
     "@hookform/resolvers": "^2.7.1",
+    "@reduxjs/toolkit": "^1.6.2",
     "@rooks/use-outside-click-ref": "^4.10.1",
     "@testing-library/react": "^12.0.0",
     "@types/eventsource": "^1.1.6",
+    "@types/styled-components": "^5.1.14",
     "@types/yup": "^0.29.13",
     "ace-builds": "^1.4.12",
     "ajv": "^8.6.3",
     "bulma": "^0.9.3",
-    "bulma-switch": "^2.0.0",
     "classnames": "^2.2.6",
     "dayjs": "^1.10.6",
     "eslint-import-resolver-node": "^0.3.5",
     "eslint-import-resolver-typescript": "^2.4.0",
+    "fetch-mock": "^9.11.0",
     "json-schema-faker": "^0.5.0-rcv.39",
     "lodash": "^4.17.21",
     "node-fetch": "^2.6.1",
@@ -28,16 +30,15 @@
     "react-datepicker": "^4.2.0",
     "react-dom": "^17.0.1",
     "react-hook-form": "7.6.9",
-    "react-json-tree": "^0.15.0",
     "react-multi-select-component": "^4.0.6",
     "react-redux": "^7.2.2",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "redux": "^4.1.1",
     "redux-thunk": "^2.3.0",
-    "reselect": "^4.0.0",
     "rimraf": "^3.0.2",
     "sass": "^1.43.4",
+    "styled-components": "^5.3.1",
     "typesafe-actions": "^5.1.0",
     "use-debounce": "^7.0.0",
     "uuid": "^8.3.1",
@@ -80,7 +81,9 @@
   "devDependencies": {
     "@jest/types": "^27.0.6",
     "@openapitools/openapi-generator-cli": "^2.4.15",
+    "@testing-library/dom": "^8.11.1",
     "@testing-library/jest-dom": "^5.14.1",
+    "@testing-library/user-event": "^13.5.0",
     "@types/classnames": "^2.2.11",
     "@types/enzyme": "^3.10.9",
     "@types/jest": "^27.0.2",
@@ -94,6 +97,7 @@
     "@types/react-router-dom": "^5.1.8",
     "@types/react-test-renderer": "^17.0.1",
     "@types/redux-mock-store": "^1.0.3",
+    "@types/styled-components": "^5.1.13",
     "@types/uuid": "^8.3.1",
     "@typescript-eslint/eslint-plugin": "^4.29.1",
     "@typescript-eslint/parser": "^4.29.1",
@@ -106,6 +110,7 @@
     "eslint-config-airbnb-typescript": "^12.3.1",
     "eslint-config-prettier": "^8.3.0",
     "eslint-plugin-import": "^2.24.0",
+    "eslint-plugin-jest-dom": "^3.9.2",
     "eslint-plugin-jsx-a11y": "^6.4.1",
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-react": "^7.21.5",
@@ -116,6 +121,7 @@
     "http-proxy-middleware": "^2.0.1",
     "husky": "^7.0.1",
     "jest-sonar-reporter": "^2.0.0",
+    "jest-styled-components": "^7.0.6",
     "lint-staged": "^11.1.2",
     "prettier": "^2.3.1",
     "react-scripts": "4.0.3",

+ 1 - 1
kafka-ui-react-app/sonar-project.properties

@@ -2,7 +2,7 @@ sonar.projectKey=provectus_kafka-ui_frontend
 sonar.organization=provectus
 
 sonar.sources=.
-sonar.exclusions="**/__test?__/**,src/setupWorker.ts,src/setupTests.ts,**/fixtures.ts"
+sonar.exclusions="**/__test?__/**,src/setupWorker.ts,src/setupTests.ts,**/fixtures.ts,src/lib/testHelpers.tsx"
 
 sonar.typescript.lcov.reportPaths=./coverage/lcov.info
 sonar.testExecutionReportPaths=./test-report.xml

+ 0 - 48
kafka-ui-react-app/src/components/Alert/Alert.tsx

@@ -1,48 +0,0 @@
-import React from 'react';
-import cx from 'classnames';
-import { useDispatch } from 'react-redux';
-import { dismissAlert } from 'redux/actions';
-import { Alert as AlertProps } from 'redux/interfaces';
-
-const Alert: React.FC<AlertProps> = ({
-  id,
-  type,
-  title,
-  message,
-  response,
-}) => {
-  const classNames = React.useMemo(
-    () =>
-      cx('notification', {
-        'is-danger': type === 'error',
-        'is-success': type === 'success',
-        'is-info': type === 'info',
-        'is-warning': type === 'warning',
-      }),
-    [type]
-  );
-  const dispatch = useDispatch();
-  const dismiss = React.useCallback(() => {
-    dispatch(dismissAlert(id));
-  }, []);
-
-  return (
-    <div className={classNames}>
-      <button className="delete" type="button" onClick={dismiss}>
-        x
-      </button>
-      <div>
-        <h6 className="title is-6">{title}</h6>
-        <p className="subtitle is-6">{message}</p>
-        {response && (
-          <div className="is-flex">
-            <div className="mr-3">{response.status}</div>
-            <div>{response.body?.message || response.statusText}</div>
-          </div>
-        )}
-      </div>
-    </div>
-  );
-};
-
-export default Alert;

+ 0 - 119
kafka-ui-react-app/src/components/Alert/__tests__/Alert.spec.tsx

@@ -1,119 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import { Alert as AlertProps } from 'redux/interfaces';
-import * as actions from 'redux/actions/actions';
-import Alert from 'components/Alert/Alert';
-
-const id = 'test-id';
-const title = 'My Alert Title';
-const message = 'My Alert Message';
-const statusCode = 123;
-const serverSideMessage = 'Server Side Message';
-const httpStatusText = 'My Status Text';
-const dismiss = jest.fn();
-
-describe('Alert', () => {
-  const setupComponent = (props: Partial<AlertProps> = {}) => (
-    <Alert
-      id={id}
-      type="error"
-      title={title}
-      message={message}
-      createdAt={1234567}
-      {...props}
-    />
-  );
-
-  it('renders with initial props', () => {
-    const wrapper = mount(setupComponent());
-    expect(wrapper.exists('.title.is-6')).toBeTruthy();
-    expect(wrapper.find('.title.is-6').text()).toEqual(title);
-    expect(wrapper.exists('.subtitle.is-6')).toBeTruthy();
-    expect(wrapper.find('.subtitle.is-6').text()).toEqual(message);
-    expect(wrapper.exists('button')).toBeTruthy();
-    expect(wrapper.exists('.is-flex')).toBeFalsy();
-  });
-
-  it('renders alert with server side message', () => {
-    const wrapper = mount(
-      setupComponent({
-        type: 'info',
-        response: {
-          status: statusCode,
-          statusText: 'My Status Text',
-          body: {
-            message: serverSideMessage,
-          },
-        },
-      })
-    );
-    expect(wrapper.exists('.is-flex')).toBeTruthy();
-    expect(wrapper.find('.is-flex').text()).toEqual(
-      `${statusCode}${serverSideMessage}`
-    );
-  });
-
-  it('renders alert with http status text', () => {
-    const wrapper = mount(
-      setupComponent({
-        type: 'info',
-        response: {
-          status: statusCode,
-          statusText: httpStatusText,
-          body: {},
-        },
-      })
-    );
-    expect(wrapper.exists('.is-flex')).toBeTruthy();
-    expect(wrapper.find('.is-flex').text()).toEqual(
-      `${statusCode}${httpStatusText}`
-    );
-  });
-
-  it('matches snapshot', () => {
-    expect(mount(setupComponent())).toMatchSnapshot();
-  });
-
-  describe('types', () => {
-    it('renders error', () => {
-      const wrapper = mount(setupComponent({ type: 'error' }));
-      expect(wrapper.exists('.notification.is-danger')).toBeTruthy();
-      expect(wrapper.exists('.notification.is-warning')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-info')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-success')).toBeFalsy();
-    });
-
-    it('renders warning', () => {
-      const wrapper = mount(setupComponent({ type: 'warning' }));
-      expect(wrapper.exists('.notification.is-warning')).toBeTruthy();
-      expect(wrapper.exists('.notification.is-danger')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-info')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-success')).toBeFalsy();
-    });
-
-    it('renders info', () => {
-      const wrapper = mount(setupComponent({ type: 'info' }));
-      expect(wrapper.exists('.notification.is-info')).toBeTruthy();
-      expect(wrapper.exists('.notification.is-warning')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-danger')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-success')).toBeFalsy();
-    });
-
-    it('renders success', () => {
-      const wrapper = mount(setupComponent({ type: 'success' }));
-      expect(wrapper.exists('.notification.is-success')).toBeTruthy();
-      expect(wrapper.exists('.notification.is-warning')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-info')).toBeFalsy();
-      expect(wrapper.exists('.notification.is-danger')).toBeFalsy();
-    });
-  });
-
-  describe('dismiss', () => {
-    it('handles dismiss callback', () => {
-      jest.spyOn(actions, 'dismissAlert').mockImplementation(dismiss);
-      const wrapper = mount(setupComponent());
-      wrapper.find('button').simulate('click');
-      expect(dismiss).toHaveBeenCalledWith(id);
-    });
-  });
-});

+ 0 - 35
kafka-ui-react-app/src/components/Alert/__tests__/__snapshots__/Alert.spec.tsx.snap

@@ -1,35 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Alert matches snapshot 1`] = `
-<Alert
-  createdAt={1234567}
-  id="test-id"
-  message="My Alert Message"
-  title="My Alert Title"
-  type="error"
->
-  <div
-    className="notification is-danger"
-  >
-    <button
-      className="delete"
-      onClick={[Function]}
-      type="button"
-    >
-      x
-    </button>
-    <div>
-      <h6
-        className="title is-6"
-      >
-        My Alert Title
-      </h6>
-      <p
-        className="subtitle is-6"
-      >
-        My Alert Message
-      </p>
-    </div>
-  </div>
-</Alert>
-`;

+ 27 - 0
kafka-ui-react-app/src/components/Alerts/Alert.styled.ts

@@ -0,0 +1,27 @@
+import { AlertType } from 'redux/interfaces';
+import styled from 'styled-components';
+
+export const Alert = styled.div<{ $type: AlertType }>`
+  background-color: ${({ $type, theme }) => theme.alert.color[$type]};
+  width: 400px;
+  min-height: 64px;
+  border-radius: 8px;
+  padding: 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  filter: drop-shadow(0px 4px 16px rgba(0, 0, 0, 0.1));
+  margin-top: 10px;
+  line-height: 20px;
+`;
+
+export const Title = styled.div`
+  font-weight: 500;
+  font-size: 14px;
+`;
+
+export const Message = styled.p`
+  font-weight: normal;
+  font-size: 14px;
+  margin: 3px 0;
+`;

+ 28 - 0
kafka-ui-react-app/src/components/Alerts/Alert.tsx

@@ -0,0 +1,28 @@
+import React from 'react';
+import CloseIcon from 'components/common/Icons/CloseIcon';
+import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
+import { Alert as AlertType } from 'redux/interfaces';
+
+import * as S from './Alert.styled';
+
+export interface AlertProps {
+  title: AlertType['title'];
+  type: AlertType['type'];
+  message: AlertType['message'];
+  onDissmiss(): void;
+}
+
+const Alert: React.FC<AlertProps> = ({ title, type, message, onDissmiss }) => (
+  <S.Alert $type={type} role="alert">
+    <div>
+      <S.Title role="heading">{title}</S.Title>
+      <S.Message role="contentinfo">{message}</S.Message>
+    </div>
+
+    <IconButtonWrapper role="button" onClick={onDissmiss}>
+      <CloseIcon />
+    </IconButtonWrapper>
+  </S.Alert>
+);
+
+export default Alert;

+ 44 - 0
kafka-ui-react-app/src/components/Alerts/Alerts.tsx

@@ -0,0 +1,44 @@
+import React from 'react';
+import { dismissAlert } from 'redux/actions';
+import { getAlerts } from 'redux/reducers/alerts/selectors';
+import { alertDissmissed, selectAll } from 'redux/reducers/alerts/alertsSlice';
+import { useAppSelector, useAppDispatch } from 'lib/hooks/redux';
+import Alert from 'components/Alerts/Alert';
+
+const Alerts: React.FC = () => {
+  const alerts = useAppSelector(selectAll);
+  const dispatch = useAppDispatch();
+  const dismiss = React.useCallback((id: string) => {
+    dispatch(alertDissmissed(id));
+  }, []);
+
+  const legacyAlerts = useAppSelector(getAlerts);
+  const dismissLegacy = React.useCallback((id: string) => {
+    dispatch(dismissAlert(id));
+  }, []);
+
+  return (
+    <>
+      {alerts.map(({ id, type, title, message }) => (
+        <Alert
+          key={id}
+          type={type}
+          title={title}
+          message={message}
+          onDissmiss={() => dismiss(id)}
+        />
+      ))}
+      {legacyAlerts.map(({ id, type, title, message }) => (
+        <Alert
+          key={id}
+          type={type}
+          title={title}
+          message={message}
+          onDissmiss={() => dismissLegacy(id)}
+        />
+      ))}
+    </>
+  );
+};
+
+export default Alerts;

+ 36 - 0
kafka-ui-react-app/src/components/Alerts/__tests__/Alert.spec.tsx

@@ -0,0 +1,36 @@
+import React from 'react';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Alert as AlertProps } from 'redux/interfaces';
+import Alert from 'components/Alerts/Alert';
+import { render } from 'lib/testHelpers';
+
+const id = 'test-id';
+const title = 'My Alert Title';
+const message = 'My Alert Message';
+const dismiss = jest.fn();
+
+describe('Alert', () => {
+  const setupComponent = (props: Partial<AlertProps> = {}) =>
+    render(
+      <Alert
+        id={id}
+        type="error"
+        title={title}
+        message={message}
+        onDissmiss={dismiss}
+        {...props}
+      />
+    );
+  it('renders with initial props', () => {
+    setupComponent();
+    expect(screen.getByRole('heading')).toHaveTextContent(title);
+    expect(screen.getByRole('contentinfo')).toHaveTextContent(message);
+    expect(screen.getByRole('button')).toBeInTheDocument();
+  });
+  it('handles dismiss callback', () => {
+    setupComponent();
+    userEvent.click(screen.getByRole('button'));
+    expect(dismiss).toHaveBeenCalled();
+  });
+});

+ 59 - 0
kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import { Action, FailurePayload, ServerResponse } from 'redux/interfaces';
+import { screen } from '@testing-library/react';
+import Alerts from 'components/Alerts/Alerts';
+import { render } from 'lib/testHelpers';
+import { store } from 'redux/store';
+import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers';
+import userEvent from '@testing-library/user-event';
+
+describe('Alerts', () => {
+  beforeEach(() => render(<Alerts />));
+
+  it('renders alerts', async () => {
+    const payload: ServerResponse = {
+      status: 422,
+      statusText: 'Unprocessable Entity',
+      message: 'Unprocessable Entity',
+      url: 'https://test.com/clusters',
+    };
+    const action: UnknownAsyncThunkRejectedWithValueAction = {
+      type: 'any/action/rejected',
+      payload,
+      meta: {
+        arg: 'test',
+        requestId: 'test-request-id',
+        requestStatus: 'rejected',
+        aborted: false,
+        condition: false,
+        rejectedWithValue: true,
+      },
+      error: { message: 'Rejected' },
+    };
+    store.dispatch(action);
+
+    const alert: FailurePayload = {
+      title: '404 - Not Found',
+      message: 'Item is not found',
+      subject: 'subject',
+    };
+    const legacyAction: Action = {
+      type: 'CLEAR_TOPIC_MESSAGES__FAILURE',
+      payload: { alert },
+    };
+    store.dispatch(legacyAction);
+
+    expect(screen.getAllByRole('alert').length).toEqual(2);
+
+    const dissmissAlertButtons = screen.getAllByRole('button');
+    expect(dissmissAlertButtons.length).toEqual(2);
+
+    const dissmissButton = dissmissAlertButtons[0];
+    const dissmissLegacyButton = dissmissAlertButtons[1];
+
+    userEvent.click(dissmissButton);
+    userEvent.click(dissmissLegacyButton);
+
+    expect(screen.queryAllByRole('alert').length).toEqual(0);
+  });
+});

+ 0 - 98
kafka-ui-react-app/src/components/App.scss

@@ -1,98 +0,0 @@
-$header-height: 52px;
-$navbar-width: 250px;
-
-.Layout {
-  min-width: 1200px;
-
-  &__header {
-    box-shadow: 0 0.46875rem 2.1875rem rgba(4,9,20,0.03),
-      0 0.9375rem 1.40625rem rgba(4,9,20,0.03),
-      0 0.25rem 0.53125rem rgba(4,9,20,0.05),
-      0 0.125rem 0.1875rem rgba(4,9,20,0.03);
-    z-index: 31;
-  }
-
-  &__container {
-    margin-top: $header-height;
-    margin-left: $navbar-width;
-    position: relative;
-    z-index: 20;
-  }
-
-  &__sidebar{
-    width: $navbar-width;
-    display: flex;
-    flex-direction: column;
-    box-shadow: 7px 0 60px rgba(0,0,0,0.05);
-    position: fixed;
-    top: $header-height;
-    left: 0;
-    bottom: 0;
-    padding: 20px 20px;
-    overflow-y: scroll;
-    transition: width .25s,opacity .25s,transform .25s,-webkit-transform .25s;
-
-    &Overlay {
-      position: fixed;
-      top: 0;
-      height: 120vh;
-      z-index: 99;
-      display: block;
-      visibility: hidden;
-      opacity: 0;
-      -webkit-transition: all .5s ease;
-      transition: all .5s ease;
-    }
-  }
-
-  &__alerts {
-    max-width: 40%;
-    width: 500px;
-    position: fixed;
-    bottom: 15px;
-    right: 15px;
-    z-index: 1000;
-  }
-}
-
-.react-datepicker-wrapper {
-  display: flex !important;
-}
-
-.react-datepicker-popper {
-  z-index: 30 !important;
-}
-
-@media screen and (max-width: 1023px) {
-  .Layout {
-    min-width: initial;
-
-    &__container {
-      margin-left: initial;
-      margin-top: 1.5rem;
-    }
-
-    &__sidebar {
-      left: -$navbar-width;
-      z-index: 100;
-    }
-
-    &__alerts {
-      max-width: initial;
-    }
-
-    &--sidebarVisible {
-      .Layout__sidebar {
-        transform: translate3d($navbar-width,0,0);
-
-        &Overlay {
-          background-color: rgba(34,41,47,.5);
-          left: 0;
-          right: 0;
-          opacity: 1;
-          visibility: visible;
-        }
-      }
-    }
-  }
-}

+ 177 - 0
kafka-ui-react-app/src/components/App.styled.ts

@@ -0,0 +1,177 @@
+import styled, { css } from 'styled-components';
+
+export const Layout = styled.div`
+  min-width: 1200px;
+
+  @media screen and (max-width: 1023px) {
+    min-width: initial;
+  }
+`;
+
+export const Container = styled.main(
+  ({ theme }) => css`
+    margin-top: ${theme.layout.navBarHeight};
+    margin-left: ${theme.layout.navBarWidth};
+    position: relative;
+    z-index: 20;
+
+    @media screen and (max-width: 1023px) {
+      margin-left: initial;
+    }
+  `
+);
+
+export const Sidebar = styled.div<{ $visible: boolean }>(
+  ({ theme, $visible }) => css`
+    width: ${theme.layout.navBarWidth};
+    display: flex;
+    flex-direction: column;
+    border-right: 1px solid #e7e7e7;
+    position: fixed;
+    top: ${theme.layout.navBarHeight};
+    left: 0;
+    bottom: 0;
+    padding: 8px 16px;
+    overflow-y: scroll;
+    transition: width 0.25s, opacity 0.25s, transform 0.25s,
+      -webkit-transform 0.25s;
+    background: ${theme.menuStyles.backgroundColor.normal};
+    @media screen and (max-width: 1023px) {
+      ${$visible &&
+      `transform: translate3d(${theme.layout.navBarWidth}, 0, 0)`};
+      left: -${theme.layout.navBarWidth};
+      z-index: 100;
+    }
+  `
+);
+
+export const Overlay = styled.div<{ $visible: boolean }>(
+  ({ theme, $visible }) => css`
+    height: calc(100vh - ${theme.layout.navBarHeight});
+    z-index: 99;
+    visibility: 'hidden';
+    opacity: 0;
+    -webkit-transition: all 0.5s ease;
+    transition: all 0.5s ease;
+    left: 0;
+    position: absolute;
+    top: 0;
+    ${$visible &&
+    css`
+      @media screen and (max-width: 1023px) {
+        bottom: 0;
+        right: 0;
+        visibility: 'visible';
+        opacity: 1;
+        background-color: rgba(34, 41, 47, 0.5);
+      }
+    `}
+  `
+);
+
+export const Navbar = styled.nav(
+  ({ theme }) => css`
+    border-bottom: 1px solid #e7e7e7;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 30;
+    background-color: ${theme.menuStyles.backgroundColor.normal};
+    min-height: 3.25rem;
+  `
+);
+
+export const NavbarBrand = styled.div`
+  display: flex;
+  flex-shrink: 0;
+  align-items: stretch;
+  min-height: 3.25rem;
+`;
+
+export const NavbarItem = styled.div`
+  display: flex;
+  position: relative;
+  flex-grow: 0;
+  flex-shrink: 0;
+  align-items: center;
+  line-height: 1.5;
+  padding: 0.5rem 0.75rem;
+`;
+
+export const NavbarBurger = styled.div(
+  ({ theme }) => css`
+    display: block;
+    position: relative;
+    cursor: pointer;
+    height: 3.25rem;
+    width: 3.25rem;
+    margin: 0;
+    padding: 0;
+
+    &:hover {
+      background-color: ${theme.menuStyles.backgroundColor.hover};
+    }
+
+    @media screen and (min-width: 1024px) {
+      display: none;
+    }
+  `
+);
+
+export const Span = styled.span(
+  ({ theme }) => css`
+    display: block;
+    position: absolute;
+    background: ${theme.menuStyles.color.active};
+    height: 1px;
+    left: calc(50% - 8px);
+    transform-origin: center;
+    transition-duration: 86ms;
+    transition-property: background-color, opacity, transform, -webkit-transform;
+    transition-timing-function: ease-out;
+    width: 16px;
+
+    &:first-child {
+      top: calc(50% - 6px);
+    }
+    &:nth-child(2) {
+      top: calc(50% - 1px);
+    }
+    &:nth-child(3) {
+      top: calc(50% + 4px);
+    }
+  `
+);
+
+export const Hyperlink = styled.a(
+  ({ theme }) => css`
+    display: flex;
+    position: relative;
+    flex-grow: 0;
+    flex-shrink: 0;
+    align-items: center;
+    margin: 0;
+    color: ${theme.menuStyles.color.active};
+    font-size: 1.25rem;
+    font-weight: 600;
+    cursor: pointer;
+    line-height: 1.5;
+    padding: 0.5rem 0.75rem;
+    text-decoration: none;
+    word-break: break-word;
+  `
+);
+
+export const AlertsContainer = styled.div`
+  max-width: 40%;
+  width: 500px;
+  position: fixed;
+  bottom: 15px;
+  left: 15px;
+  z-index: 1000;
+
+  @media screen and (max-width: 1023px) {
+    max-width: initial;
+  }
+`;

+ 74 - 89
kafka-ui-react-app/src/components/App.tsx

@@ -1,30 +1,28 @@
 import React from 'react';
-import cx from 'classnames';
-import { Cluster } from 'generated-sources';
 import { Switch, Route, useLocation } from 'react-router-dom';
 import { GIT_TAG, GIT_COMMIT } from 'lib/constants';
-import { Alerts } from 'redux/interfaces';
 import Nav from 'components/Nav/Nav';
 import PageLoader from 'components/common/PageLoader/PageLoader';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import Dashboard from 'components/Dashboard/Dashboard';
 import ClusterPage from 'components/Cluster/Cluster';
 import Version from 'components/Version/Version';
-import Alert from 'components/Alert/Alert';
-import 'components/App.scss';
+import Alerts from 'components/Alerts/Alerts';
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import {
+  fetchClusters,
+  getClusterList,
+  getAreClustersFulfilled,
+} from 'redux/reducers/clusters/clustersSlice';
 
-export interface AppProps {
-  isClusterListFetched?: boolean;
-  alerts: Alerts;
-  clusters: Cluster[];
-  fetchClustersList: () => void;
-}
+import * as S from './App.styled';
 
-const App: React.FC<AppProps> = ({
-  isClusterListFetched,
-  alerts,
-  clusters,
-  fetchClustersList,
-}) => {
+const App: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const areClustersFulfilled = useAppSelector(getAreClustersFulfilled);
+  const clusters = useAppSelector(getClusterList);
   const [isSidebarVisible, setIsSidebarVisible] = React.useState(false);
 
   const onBurgerClick = React.useCallback(
@@ -41,85 +39,72 @@ const App: React.FC<AppProps> = ({
   }, [location]);
 
   React.useEffect(() => {
-    fetchClustersList();
-  }, [fetchClustersList]);
+    dispatch(fetchClusters());
+  }, [fetchClusters]);
 
   return (
-    <div
-      className={cx('Layout', { 'Layout--sidebarVisible': isSidebarVisible })}
-    >
-      <nav
-        className="navbar is-fixed-top is-white Layout__header"
-        role="navigation"
-        aria-label="main navigation"
-      >
-        <div className="navbar-brand">
-          <div
-            className={cx('navbar-burger', 'ml-0', {
-              'is-active': isSidebarVisible,
-            })}
-            onClick={onBurgerClick}
-            onKeyDown={onBurgerClick}
-            role="button"
-            tabIndex={0}
-          >
-            <span />
-            <span />
-            <span />
-          </div>
+    <ThemeProvider theme={theme}>
+      <S.Layout>
+        <S.Navbar role="navigation" aria-label="Page Header">
+          <S.NavbarBrand>
+            <S.NavbarBurger
+              onClick={onBurgerClick}
+              onKeyDown={onBurgerClick}
+              role="button"
+              tabIndex={0}
+            >
+              <S.Span role="separator" />
+              <S.Span role="separator" />
+              <S.Span role="separator" />
+            </S.NavbarBurger>
 
-          <a className="navbar-item title is-5 is-marginless" href="/ui">
-            UI for Apache Kafka
-          </a>
+            <S.Hyperlink href="/ui">UI for Apache Kafka</S.Hyperlink>
 
-          <div className="navbar-item">
-            <Version tag={GIT_TAG} commit={GIT_COMMIT} />
-          </div>
-        </div>
-      </nav>
+            <S.NavbarItem>
+              <Version tag={GIT_TAG} commit={GIT_COMMIT} />
+            </S.NavbarItem>
+          </S.NavbarBrand>
+        </S.Navbar>
 
-      <main className="Layout__container">
-        <div className="Layout__sidebar has-shadow has-background-white">
-          <Nav
-            clusters={clusters}
-            isClusterListFetched={isClusterListFetched}
-          />
-        </div>
-        <div
-          className="Layout__sidebarOverlay is-overlay"
-          onClick={closeSidebar}
-          onKeyDown={closeSidebar}
-          tabIndex={-1}
-          aria-hidden="true"
-        />
-        {isClusterListFetched ? (
-          <Switch>
-            <Route
-              exact
-              path={['/', '/ui', '/ui/clusters']}
-              component={Dashboard}
+        <S.Container>
+          <S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
+            <Nav
+              clusters={clusters}
+              areClustersFulfilled={areClustersFulfilled}
             />
-            <Route path="/ui/clusters/:clusterName" component={ClusterPage} />
-          </Switch>
-        ) : (
-          <PageLoader fullHeight />
-        )}
-      </main>
-
-      <div className="Layout__alerts">
-        {alerts.map(({ id, type, title, message, response, createdAt }) => (
-          <Alert
-            key={id}
-            id={id}
-            type={type}
-            title={title}
-            message={message}
-            response={response}
-            createdAt={createdAt}
+          </S.Sidebar>
+          <S.Overlay
+            $visible={isSidebarVisible}
+            onClick={closeSidebar}
+            onKeyDown={closeSidebar}
+            tabIndex={-1}
+            aria-hidden="true"
+            aria-label="Overlay"
           />
-        ))}
-      </div>
-    </div>
+          {areClustersFulfilled ? (
+            <>
+              <Breadcrumb />
+              <Switch>
+                <Route
+                  exact
+                  path={['/', '/ui', '/ui/clusters']}
+                  component={Dashboard}
+                />
+                <Route
+                  path="/ui/clusters/:clusterName"
+                  component={ClusterPage}
+                />
+              </Switch>
+            </>
+          ) : (
+            <PageLoader />
+          )}
+        </S.Container>
+        <S.AlertsContainer role="toolbar">
+          <Alerts />
+        </S.AlertsContainer>
+      </S.Layout>
+    </ThemeProvider>
   );
 };
 

+ 0 - 21
kafka-ui-react-app/src/components/AppContainer.tsx

@@ -1,21 +0,0 @@
-import { connect } from 'react-redux';
-import { fetchClustersList } from 'redux/actions';
-import {
-  getClusterList,
-  getIsClusterListFetched,
-} from 'redux/reducers/clusters/selectors';
-import { getAlerts } from 'redux/reducers/alerts/selectors';
-import { RootState } from 'redux/interfaces';
-import App from 'components/App';
-
-const mapStateToProps = (state: RootState) => ({
-  isClusterListFetched: getIsClusterListFetched(state),
-  alerts: getAlerts(state),
-  clusters: getClusterList(state),
-});
-
-const mapDispatchToProps = {
-  fetchClustersList,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(App);

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

@@ -1,40 +1,38 @@
 import React from 'react';
 import { ClusterName, ZooKeeperStatus } from 'redux/interfaces';
-import { ClusterStats } from 'generated-sources';
 import useInterval from 'lib/hooks/useInterval';
-import cx from 'classnames';
-import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
-import Indicator from 'components/common/Dashboard/Indicator';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
 import { useParams } from 'react-router';
-
-interface Props extends ClusterStats {
-  isFetched: boolean;
-  fetchClusterStats: (clusterName: ClusterName) => void;
-  fetchBrokers: (clusterName: ClusterName) => void;
-}
-
-const Brokers: React.FC<Props> = ({
-  brokerCount,
-  activeControllers,
-  zooKeeperStatus,
-  onlinePartitionCount,
-  offlinePartitionCount,
-  inSyncReplicasCount,
-  outOfSyncReplicasCount,
-  underReplicatedPartitionCount,
-  diskUsage,
+import TagStyled from 'components/common/Tag/Tag.styled';
+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 {
   fetchClusterStats,
-  fetchBrokers,
-  version,
-}) => {
+  selectStats,
+} from 'redux/reducers/brokers/brokersSlice';
+
+const Brokers: React.FC = () => {
+  const dispatch = useAppDispatch();
   const { clusterName } = useParams<{ clusterName: ClusterName }>();
+  const {
+    brokerCount,
+    activeControllers,
+    zooKeeperStatus,
+    onlinePartitionCount,
+    offlinePartitionCount,
+    inSyncReplicasCount,
+    outOfSyncReplicasCount,
+    underReplicatedPartitionCount,
+    diskUsage,
+    version,
+  } = useAppSelector(selectStats);
 
   React.useEffect(() => {
-    fetchClusterStats(clusterName);
-    fetchBrokers(clusterName);
-  }, [fetchClusterStats, fetchBrokers, clusterName]);
+    dispatch(fetchClusterStats(clusterName));
+  }, [fetchClusterStats, clusterName]);
 
   useInterval(() => {
     fetchClusterStats(clusterName);
@@ -43,61 +41,67 @@ const Brokers: React.FC<Props> = ({
   const zkOnline = zooKeeperStatus === ZooKeeperStatus.online;
 
   return (
-    <div className="section">
-      <Breadcrumb>Brokers overview</Breadcrumb>
-      <MetricsWrapper title="Uptime">
-        <Indicator className="is-one-third" label="Total Brokers">
-          {brokerCount}
-        </Indicator>
-        <Indicator className="is-one-third" label="Active Controllers">
-          {activeControllers}
-        </Indicator>
-        <Indicator className="is-one-third" label="Zookeeper Status">
-          <span className={cx('tag', zkOnline ? 'is-success' : 'is-danger')}>
-            {zkOnline ? 'Online' : 'Offline'}
-          </span>
-        </Indicator>
-        <Indicator className="is-one-third" label="Version">
-          {version}
-        </Indicator>
-      </MetricsWrapper>
-      <MetricsWrapper title="Partitions">
-        <Indicator label="Online">
-          <span
-            className={cx({ 'has-text-danger': offlinePartitionCount !== 0 })}
-          >
-            {onlinePartitionCount}
-          </span>
-          <span className="subtitle has-text-weight-light">
-            {' '}
-            of
-            {(onlinePartitionCount || 0) + (offlinePartitionCount || 0)}
-          </span>
-        </Indicator>
-        <Indicator label="URP" title="Under replicated partitions">
-          {underReplicatedPartitionCount}
-        </Indicator>
-        <Indicator label="In Sync Replicas">{inSyncReplicasCount}</Indicator>
-        <Indicator label="Out of Sync Replicas">
-          {outOfSyncReplicasCount}
-        </Indicator>
-      </MetricsWrapper>
-      <MetricsWrapper multiline title="Disk Usage">
-        {diskUsage?.map((brokerDiskUsage) => (
-          <React.Fragment key={brokerDiskUsage.brokerId}>
-            <Indicator className="is-one-third" label="Broker">
-              {brokerDiskUsage.brokerId}
-            </Indicator>
-            <Indicator className="is-one-third" label="Segment Size" title="">
-              <BytesFormatted value={brokerDiskUsage.segmentSize} />
-            </Indicator>
-            <Indicator className="is-one-third" label="Segment count">
-              {brokerDiskUsage.segmentCount}
-            </Indicator>
-          </React.Fragment>
-        ))}
-      </MetricsWrapper>
-    </div>
+    <>
+      <PageHeading text="Brokers" />
+      <Metrics.Wrapper>
+        <Metrics.Section title="Uptime">
+          <Metrics.Indicator label="Total Brokers">
+            {brokerCount}
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Active Controllers">
+            {activeControllers}
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Zookeeper Status">
+            <TagStyled color={zkOnline ? 'green' : 'gray'}>
+              {zkOnline ? 'online' : 'offline'}
+            </TagStyled>
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Version">{version}</Metrics.Indicator>
+        </Metrics.Section>
+        <Metrics.Section title="Partitions">
+          <Metrics.Indicator label="Online" isAlert>
+            {offlinePartitionCount && offlinePartitionCount > 0 ? (
+              <Metrics.RedText>{onlinePartitionCount}</Metrics.RedText>
+            ) : (
+              onlinePartitionCount
+            )}
+            <Metrics.LightText>
+              {' '}
+              of {(onlinePartitionCount || 0) + (offlinePartitionCount || 0)}
+            </Metrics.LightText>
+          </Metrics.Indicator>
+          <Metrics.Indicator label="URP" title="Under replicated partitions">
+            {underReplicatedPartitionCount}
+          </Metrics.Indicator>
+          <Metrics.Indicator label="In Sync Replicas">
+            {inSyncReplicasCount}
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Out of Sync Replicas">
+            {outOfSyncReplicasCount}
+          </Metrics.Indicator>
+        </Metrics.Section>
+      </Metrics.Wrapper>
+      <Table isFullwidth>
+        <thead>
+          <tr>
+            <TableHeaderCell title="Broker" />
+            <TableHeaderCell title="Segment size (Mb)" />
+            <TableHeaderCell title="Segment Count" />
+          </tr>
+        </thead>
+        <tbody>
+          {diskUsage?.map(({ brokerId, segmentSize, segmentCount }) => (
+            <tr key={brokerId}>
+              <td>{brokerId}</td>
+              <td>
+                <BytesFormatted value={segmentSize} />
+              </td>
+              <td>{segmentCount}</td>
+            </tr>
+          ))}
+        </tbody>
+      </Table>
+    </>
   );
 };
 

+ 0 - 38
kafka-ui-react-app/src/components/Brokers/BrokersContainer.ts

@@ -1,38 +0,0 @@
-import { connect } from 'react-redux';
-import { fetchClusterStats, fetchBrokers } from 'redux/actions';
-import { RootState } from 'redux/interfaces';
-import {
-  getIsBrokerListFetched,
-  getBrokerCount,
-  getZooKeeperStatus,
-  getActiveControllers,
-  getOnlinePartitionCount,
-  getOfflinePartitionCount,
-  getInSyncReplicasCount,
-  getOutOfSyncReplicasCount,
-  getUnderReplicatedPartitionCount,
-  getDiskUsage,
-  getVersion,
-} from 'redux/reducers/brokers/selectors';
-import Brokers from 'components/Brokers/Brokers';
-
-const mapStateToProps = (state: RootState) => ({
-  isFetched: getIsBrokerListFetched(state),
-  brokerCount: getBrokerCount(state),
-  zooKeeperStatus: getZooKeeperStatus(state),
-  activeControllers: getActiveControllers(state),
-  onlinePartitionCount: getOnlinePartitionCount(state),
-  offlinePartitionCount: getOfflinePartitionCount(state),
-  inSyncReplicasCount: getInSyncReplicasCount(state),
-  outOfSyncReplicasCount: getOutOfSyncReplicasCount(state),
-  underReplicatedPartitionCount: getUnderReplicatedPartitionCount(state),
-  diskUsage: getDiskUsage(state),
-  version: getVersion(state),
-});
-
-const mapDispatchToProps = {
-  fetchClusterStats,
-  fetchBrokers,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Brokers);

+ 41 - 77
kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx

@@ -1,90 +1,54 @@
 import React from 'react';
-import { mount } from 'enzyme';
 import Brokers from 'components/Brokers/Brokers';
-import { ClusterName } from 'redux/interfaces';
-import { StaticRouter } from 'react-router';
-import { ClusterStats } from 'generated-sources';
-
-interface Props extends ClusterStats {
-  isFetched: boolean;
-  fetchClusterStats: (clusterName: ClusterName) => void;
-  fetchBrokers: (clusterName: ClusterName) => void;
-}
+import { render } from 'lib/testHelpers';
+import { screen, waitFor } from '@testing-library/dom';
+import { Route, StaticRouter } from 'react-router';
+import { clusterBrokersPath } from 'lib/paths';
+import fetchMock from 'fetch-mock';
+import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures';
 
 describe('Brokers Component', () => {
-  const pathname = `ui/clusters/local/brokers`;
-
-  describe('Brokers Empty', () => {
-    const setupEmptyComponent = (props: Partial<Props> = {}) => (
-      <StaticRouter location={{ pathname }} context={{}}>
-        <Brokers
-          brokerCount={0}
-          activeControllers={0}
-          zooKeeperStatus={0}
-          onlinePartitionCount={0}
-          offlinePartitionCount={0}
-          inSyncReplicasCount={0}
-          outOfSyncReplicasCount={0}
-          underReplicatedPartitionCount={0}
-          version="1"
-          fetchClusterStats={jest.fn()}
-          fetchBrokers={jest.fn()}
-          diskUsage={undefined}
-          isFetched={false}
-          {...props}
-        />
+  afterEach(() => fetchMock.reset());
+
+  const clusterName = 'local';
+  const renderComponent = () =>
+    render(
+      <StaticRouter
+        location={{
+          pathname: clusterBrokersPath(clusterName),
+        }}
+      >
+        <Route path={clusterBrokersPath(':clusterName')}>
+          <Brokers />
+        </Route>
       </StaticRouter>
     );
-    it('renders section', () => {
-      const component = mount(setupEmptyComponent());
-      expect(component.exists('.section')).toBeTruthy();
-    });
-
-    it('renders section with is-danger selector', () => {
-      const component = mount(setupEmptyComponent());
-      expect(component.exists('.is-danger')).toBeTruthy();
-    });
-
-    it('matches Brokers Empty snapshot', () => {
-      expect(mount(setupEmptyComponent())).toMatchSnapshot();
-    });
-  });
 
   describe('Brokers', () => {
-    const setupComponent = (props: Partial<Props> = {}) => (
-      <StaticRouter location={{ pathname }} context={{}}>
-        <Brokers
-          brokerCount={1}
-          activeControllers={1}
-          zooKeeperStatus={1}
-          onlinePartitionCount={64}
-          offlinePartitionCount={0}
-          inSyncReplicasCount={64}
-          outOfSyncReplicasCount={0}
-          underReplicatedPartitionCount={0}
-          version="1"
-          fetchClusterStats={jest.fn()}
-          fetchBrokers={jest.fn()}
-          diskUsage={[
-            {
-              brokerId: 1,
-              segmentCount: 64,
-              segmentSize: 60718,
-            },
-          ]}
-          isFetched
-          {...props}
-        />
-      </StaticRouter>
-    );
-
-    it('renders section with is-success selector', () => {
-      const component = mount(setupComponent());
-      expect(component.exists('.is-success')).toBeTruthy();
+    it('renders', async () => {
+      const mock = fetchMock.getOnce(
+        `/api/clusters/${clusterName}/stats`,
+        clusterStatsPayload
+      );
+      renderComponent();
+      await waitFor(() => expect(mock.called()).toBeTruthy());
+      expect(screen.getByRole('table')).toBeInTheDocument();
+      const rows = screen.getAllByRole('row');
+      expect(rows.length).toEqual(3);
     });
 
-    it('matches snapshot', () => {
-      expect(mount(setupComponent())).toMatchSnapshot();
+    it('shows warning when offlinePartitionCount > 0', async () => {
+      const mock = fetchMock.getOnce(`/api/clusters/${clusterName}/stats`, {
+        ...clusterStatsPayload,
+        offlinePartitionCount: 1345,
+      });
+      renderComponent();
+      await waitFor(() => expect(mock.called()).toBeTruthy());
+      const onlineWidget = screen.getByText(
+        clusterStatsPayload.onlinePartitionCount
+      );
+      expect(onlineWidget).toBeInTheDocument();
+      expect(onlineWidget).toHaveStyle({ color: '#E51A1A' });
     });
   });
 });

+ 0 - 8
kafka-ui-react-app/src/components/Brokers/__test__/BrokersContainer.spec.tsx

@@ -1,8 +0,0 @@
-import React from 'react';
-import { containerRendersView } from 'lib/testHelpers';
-import Brokers from 'components/Brokers/Brokers';
-import BrokersContainer from 'components/Brokers/BrokersContainer';
-
-describe('BrokersContainer', () => {
-  containerRendersView(<BrokersContainer />, Brokers);
-});

+ 0 - 779
kafka-ui-react-app/src/components/Brokers/__test__/__snapshots__/Brokers.spec.tsx.snap

@@ -1,779 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Brokers Component Brokers Empty matches Brokers Empty snapshot 1`] = `
-<StaticRouter
-  context={Object {}}
-  location={
-    Object {
-      "pathname": "ui/clusters/local/brokers",
-    }
-  }
->
-  <Router
-    history={
-      Object {
-        "action": "POP",
-        "block": [Function],
-        "createHref": [Function],
-        "go": [Function],
-        "goBack": [Function],
-        "goForward": [Function],
-        "listen": [Function],
-        "location": Object {
-          "hash": "",
-          "pathname": "ui/clusters/local/brokers",
-          "search": "",
-        },
-        "push": [Function],
-        "replace": [Function],
-      }
-    }
-    staticContext={Object {}}
-  >
-    <Brokers
-      activeControllers={0}
-      brokerCount={0}
-      fetchBrokers={
-        [MockFunction] {
-          "calls": Array [
-            Array [
-              undefined,
-            ],
-          ],
-          "results": Array [
-            Object {
-              "type": "return",
-              "value": undefined,
-            },
-          ],
-        }
-      }
-      fetchClusterStats={
-        [MockFunction] {
-          "calls": Array [
-            Array [
-              undefined,
-            ],
-          ],
-          "results": Array [
-            Object {
-              "type": "return",
-              "value": undefined,
-            },
-          ],
-        }
-      }
-      inSyncReplicasCount={0}
-      isFetched={false}
-      offlinePartitionCount={0}
-      onlinePartitionCount={0}
-      outOfSyncReplicasCount={0}
-      underReplicatedPartitionCount={0}
-      version="1"
-      zooKeeperStatus={0}
-    >
-      <div
-        className="section"
-      >
-        <Breadcrumb>
-          <nav
-            aria-label="breadcrumbs"
-            className="breadcrumb"
-          >
-            <ul>
-              <li
-                className="is-active"
-              >
-                <span
-                  className=""
-                >
-                  Brokers overview
-                </span>
-              </li>
-            </ul>
-          </nav>
-        </Breadcrumb>
-        <MetricsWrapper
-          title="Uptime"
-        >
-          <div
-            className="box"
-          >
-            <h5
-              className="subtitle is-6"
-            >
-              Uptime
-            </h5>
-            <div
-              className="level"
-            >
-              <Indicator
-                className="is-one-third"
-                label="Total Brokers"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Total Brokers"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Total Brokers
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      0
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Active Controllers"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Active Controllers"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Active Controllers
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      0
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Zookeeper Status"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Zookeeper Status"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Zookeeper Status
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      <span
-                        className="tag is-danger"
-                      >
-                        Offline
-                      </span>
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Version"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Version"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Version
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      1
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-            </div>
-          </div>
-        </MetricsWrapper>
-        <MetricsWrapper
-          title="Partitions"
-        >
-          <div
-            className="box"
-          >
-            <h5
-              className="subtitle is-6"
-            >
-              Partitions
-            </h5>
-            <div
-              className="level"
-            >
-              <Indicator
-                label="Online"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="Online"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Online
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      <span
-                        className=""
-                      >
-                        0
-                      </span>
-                      <span
-                        className="subtitle has-text-weight-light"
-                      >
-                         
-                        of
-                        0
-                      </span>
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                label="URP"
-                title="Under replicated partitions"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="Under replicated partitions"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      URP
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      0
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                label="In Sync Replicas"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="In Sync Replicas"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      In Sync Replicas
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      0
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                label="Out of Sync Replicas"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="Out of Sync Replicas"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Out of Sync Replicas
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      0
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-            </div>
-          </div>
-        </MetricsWrapper>
-        <MetricsWrapper
-          multiline={true}
-          title="Disk Usage"
-        >
-          <div
-            className="box"
-          >
-            <h5
-              className="subtitle is-6"
-            >
-              Disk Usage
-            </h5>
-            <div
-              className="level level-multiline"
-            />
-          </div>
-        </MetricsWrapper>
-      </div>
-    </Brokers>
-  </Router>
-</StaticRouter>
-`;
-
-exports[`Brokers Component Brokers matches snapshot 1`] = `
-<StaticRouter
-  context={Object {}}
-  location={
-    Object {
-      "pathname": "ui/clusters/local/brokers",
-    }
-  }
->
-  <Router
-    history={
-      Object {
-        "action": "POP",
-        "block": [Function],
-        "createHref": [Function],
-        "go": [Function],
-        "goBack": [Function],
-        "goForward": [Function],
-        "listen": [Function],
-        "location": Object {
-          "hash": "",
-          "pathname": "ui/clusters/local/brokers",
-          "search": "",
-        },
-        "push": [Function],
-        "replace": [Function],
-      }
-    }
-    staticContext={Object {}}
-  >
-    <Brokers
-      activeControllers={1}
-      brokerCount={1}
-      diskUsage={
-        Array [
-          Object {
-            "brokerId": 1,
-            "segmentCount": 64,
-            "segmentSize": 60718,
-          },
-        ]
-      }
-      fetchBrokers={
-        [MockFunction] {
-          "calls": Array [
-            Array [
-              undefined,
-            ],
-          ],
-          "results": Array [
-            Object {
-              "type": "return",
-              "value": undefined,
-            },
-          ],
-        }
-      }
-      fetchClusterStats={
-        [MockFunction] {
-          "calls": Array [
-            Array [
-              undefined,
-            ],
-          ],
-          "results": Array [
-            Object {
-              "type": "return",
-              "value": undefined,
-            },
-          ],
-        }
-      }
-      inSyncReplicasCount={64}
-      isFetched={true}
-      offlinePartitionCount={0}
-      onlinePartitionCount={64}
-      outOfSyncReplicasCount={0}
-      underReplicatedPartitionCount={0}
-      version="1"
-      zooKeeperStatus={1}
-    >
-      <div
-        className="section"
-      >
-        <Breadcrumb>
-          <nav
-            aria-label="breadcrumbs"
-            className="breadcrumb"
-          >
-            <ul>
-              <li
-                className="is-active"
-              >
-                <span
-                  className=""
-                >
-                  Brokers overview
-                </span>
-              </li>
-            </ul>
-          </nav>
-        </Breadcrumb>
-        <MetricsWrapper
-          title="Uptime"
-        >
-          <div
-            className="box"
-          >
-            <h5
-              className="subtitle is-6"
-            >
-              Uptime
-            </h5>
-            <div
-              className="level"
-            >
-              <Indicator
-                className="is-one-third"
-                label="Total Brokers"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Total Brokers"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Total Brokers
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      1
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Active Controllers"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Active Controllers"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Active Controllers
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      1
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Zookeeper Status"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Zookeeper Status"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Zookeeper Status
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      <span
-                        className="tag is-success"
-                      >
-                        Online
-                      </span>
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Version"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Version"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Version
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      1
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-            </div>
-          </div>
-        </MetricsWrapper>
-        <MetricsWrapper
-          title="Partitions"
-        >
-          <div
-            className="box"
-          >
-            <h5
-              className="subtitle is-6"
-            >
-              Partitions
-            </h5>
-            <div
-              className="level"
-            >
-              <Indicator
-                label="Online"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="Online"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Online
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      <span
-                        className=""
-                      >
-                        64
-                      </span>
-                      <span
-                        className="subtitle has-text-weight-light"
-                      >
-                         
-                        of
-                        64
-                      </span>
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                label="URP"
-                title="Under replicated partitions"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="Under replicated partitions"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      URP
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      0
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                label="In Sync Replicas"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="In Sync Replicas"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      In Sync Replicas
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      64
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                label="Out of Sync Replicas"
-              >
-                <div
-                  className="level-item"
-                >
-                  <div
-                    title="Out of Sync Replicas"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Out of Sync Replicas
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      0
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-            </div>
-          </div>
-        </MetricsWrapper>
-        <MetricsWrapper
-          multiline={true}
-          title="Disk Usage"
-        >
-          <div
-            className="box"
-          >
-            <h5
-              className="subtitle is-6"
-            >
-              Disk Usage
-            </h5>
-            <div
-              className="level level-multiline"
-            >
-              <Indicator
-                className="is-one-third"
-                label="Broker"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Broker"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Broker
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      1
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Segment Size"
-                title=""
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Segment Size"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Segment Size
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      <BytesFormatted
-                        value={60718}
-                      >
-                        <span>
-                          59KB
-                        </span>
-                      </BytesFormatted>
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-              <Indicator
-                className="is-one-third"
-                label="Segment count"
-              >
-                <div
-                  className="level-item is-one-third"
-                >
-                  <div
-                    title="Segment count"
-                  >
-                    <p
-                      className="heading"
-                    >
-                      Segment count
-                    </p>
-                    <p
-                      className="title has-text-centered"
-                    >
-                      64
-                    </p>
-                  </div>
-                </div>
-              </Indicator>
-            </div>
-          </div>
-        </MetricsWrapper>
-      </div>
-    </Brokers>
-  </Router>
-</StaticRouter>
-`;

+ 5 - 8
kafka-ui-react-app/src/components/Cluster/Cluster.tsx

@@ -5,7 +5,7 @@ import { ClusterFeaturesEnum } from 'generated-sources';
 import {
   getClustersFeatures,
   getClustersReadonlyStatus,
-} from 'redux/reducers/clusters/selectors';
+} from 'redux/reducers/clusters/clustersSlice';
 import {
   clusterBrokersPath,
   clusterConnectorsPath,
@@ -19,8 +19,8 @@ import Topics from 'components/Topics/Topics';
 import Schemas from 'components/Schemas/Schemas';
 import Connect from 'components/Connect/Connect';
 import ClusterContext from 'components/contexts/ClusterContext';
-import BrokersContainer from 'components/Brokers/BrokersContainer';
-import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer';
+import Brokers from 'components/Brokers/Brokers';
+import ConsumersGroups from 'components/ConsumerGroups/ConsumerGroups';
 import KsqlDb from 'components/KsqlDb/KsqlDb';
 
 const Cluster: React.FC = () => {
@@ -52,14 +52,11 @@ const Cluster: React.FC = () => {
   return (
     <ClusterContext.Provider value={contextValue}>
       <Switch>
-        <Route
-          path={clusterBrokersPath(':clusterName')}
-          component={BrokersContainer}
-        />
+        <Route path={clusterBrokersPath(':clusterName')} component={Brokers} />
         <Route path={clusterTopicsPath(':clusterName')} component={Topics} />
         <Route
           path={clusterConsumerGroupsPath(':clusterName')}
-          component={ConsumersGroupsContainer}
+          component={ConsumersGroups}
         />
         {hasSchemaRegistryConfigured && (
           <Route

+ 81 - 52
kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx

@@ -1,85 +1,114 @@
 import React from 'react';
-import { mount } from 'enzyme';
-import { Provider } from 'react-redux';
 import { Route, StaticRouter } from 'react-router-dom';
 import { ClusterFeaturesEnum } from 'generated-sources';
-import { fetchClusterListAction } from 'redux/actions';
-import configureStore from 'redux/store/configureStore';
+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 {
+  clusterBrokersPath,
+  clusterConnectsPath,
+  clusterConsumerGroupsPath,
+  clusterKsqlDbPath,
+  clusterSchemasPath,
+  clusterTopicsPath,
+} from 'lib/paths';
 
-const store = configureStore();
-
-jest.mock('components/Topics/Topics', () => 'mock-Topics');
-jest.mock('components/Schemas/Schemas', () => 'mock-Schemas');
-jest.mock('components/Connect/Connect', () => 'mock-Connect');
-jest.mock('components/Brokers/BrokersContainer', () => 'mock-Brokers');
-jest.mock(
-  'components/ConsumerGroups/ConsumersGroupsContainer',
-  () => 'mock-ConsumerGroups'
-);
+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>);
+jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => (
+  <div>ConsumerGroups</div>
+));
+jest.mock('components/KsqlDb/KsqlDb', () => () => <div>KsqlDb</div>);
 
 describe('Cluster', () => {
-  const setupComponent = (pathname: string) => (
-    <Provider store={store}>
+  const renderComponent = (pathname: string) =>
+    render(
       <StaticRouter location={{ pathname }}>
         <Route path="/ui/clusters/:clusterName">
           <Cluster />
         </Route>
       </StaticRouter>
-    </Provider>
-  );
+    );
+
   it('renders Brokers', () => {
-    const wrapper = mount(setupComponent('/ui/clusters/secondLocal/brokers'));
-    expect(wrapper.exists('mock-Brokers')).toBeTruthy();
+    renderComponent(clusterBrokersPath('second'));
+    expect(screen.getByText('Brokers')).toBeInTheDocument();
   });
   it('renders Topics', () => {
-    const wrapper = mount(setupComponent('/ui/clusters/secondLocal/topics'));
-    expect(wrapper.exists('mock-Topics')).toBeTruthy();
+    renderComponent(clusterTopicsPath('second'));
+    expect(screen.getByText('Topics')).toBeInTheDocument();
   });
   it('renders ConsumerGroups', () => {
-    const wrapper = mount(
-      setupComponent('/ui/clusters/secondLocal/consumer-groups')
-    );
-    expect(wrapper.exists('mock-ConsumerGroups')).toBeTruthy();
+    renderComponent(clusterConsumerGroupsPath('second'));
+    expect(screen.getByText('ConsumerGroups')).toBeInTheDocument();
   });
 
   describe('configured features', () => {
     it('does not render Schemas if SCHEMA_REGISTRY is not configured', () => {
-      const wrapper = mount(setupComponent('/ui/clusters/secondLocal/schemas'));
-      expect(wrapper.exists('mock-Schemas')).toBeFalsy();
-    });
-    it('renders Schemas if SCHEMA_REGISTRY is configured', () => {
       store.dispatch(
-        fetchClusterListAction.success([
-          {
-            ...onlineClusterPayload,
-            features: [ClusterFeaturesEnum.SCHEMA_REGISTRY],
-          },
-        ])
+        fetchClusters.fulfilled(
+          [
+            {
+              ...onlineClusterPayload,
+              features: [],
+            },
+          ],
+          '123'
+        )
       );
-      const wrapper = mount(setupComponent('/ui/clusters/secondLocal/schemas'));
-      expect(wrapper.exists('mock-Schemas')).toBeTruthy();
+      renderComponent(clusterSchemasPath('second'));
+      expect(screen.queryByText('Schemas')).not.toBeInTheDocument();
     });
-    it('does not render Connect if KAFKA_CONNECT is not configured', () => {
-      const wrapper = mount(
-        setupComponent('/ui/clusters/secondLocal/connectors')
+    it('renders Schemas if SCHEMA_REGISTRY is configured', async () => {
+      store.dispatch(
+        fetchClusters.fulfilled(
+          [
+            {
+              ...onlineClusterPayload,
+              features: [ClusterFeaturesEnum.SCHEMA_REGISTRY],
+            },
+          ],
+          '123'
+        )
       );
-      expect(wrapper.exists('mock-Connect')).toBeFalsy();
+      renderComponent(clusterSchemasPath(onlineClusterPayload.name));
+      expect(screen.getByText('Schemas')).toBeInTheDocument();
     });
-    it('renders Schemas if KAFKA_CONNECT is configured', async () => {
+    it('renders Connect if KAFKA_CONNECT is configured', async () => {
       store.dispatch(
-        fetchClusterListAction.success([
-          {
-            ...onlineClusterPayload,
-            features: [ClusterFeaturesEnum.KAFKA_CONNECT],
-          },
-        ])
+        fetchClusters.fulfilled(
+          [
+            {
+              ...onlineClusterPayload,
+              features: [ClusterFeaturesEnum.KAFKA_CONNECT],
+            },
+          ],
+          'requestId'
+        )
       );
-      const wrapper = mount(
-        setupComponent('/ui/clusters/secondLocal/connectors')
+      renderComponent(clusterConnectsPath(onlineClusterPayload.name));
+      expect(screen.getByText('Connect')).toBeInTheDocument();
+    });
+    it('renders KSQL if KSQL_DB is configured', async () => {
+      store.dispatch(
+        fetchClusters.fulfilled(
+          [
+            {
+              ...onlineClusterPayload,
+              features: [ClusterFeaturesEnum.KSQL_DB],
+            },
+          ],
+          'requestId'
+        )
       );
-      expect(wrapper.exists('mock-Connect')).toBeTruthy();
+      renderComponent(clusterKsqlDbPath(onlineClusterPayload.name));
+      expect(screen.getByText('KsqlDb')).toBeInTheDocument();
     });
   });
 });

+ 0 - 71
kafka-ui-react-app/src/components/Connect/Breadcrumbs/Breadcrumbs.tsx

@@ -1,71 +0,0 @@
-import React from 'react';
-import { Route, Switch, useParams } from 'react-router-dom';
-import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
-import {
-  clusterConnectorsPath,
-  clusterConnectorNewPath,
-  clusterConnectConnectorPath,
-  clusterConnectConnectorEditPath,
-} from 'lib/paths';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
-
-interface RouterParams {
-  clusterName: ClusterName;
-  connectName: ConnectName;
-  connectorName: ConnectorName;
-}
-
-const Breadcrumbs: React.FC = () => {
-  const { clusterName, connectName, connectorName } = useParams<RouterParams>();
-
-  const rootLinks = [
-    {
-      href: clusterConnectorsPath(clusterName),
-      label: 'All Connectors',
-    },
-  ];
-
-  const connectorLinks = [
-    ...rootLinks,
-    {
-      href: clusterConnectConnectorPath(
-        clusterName,
-        connectName,
-        connectorName
-      ),
-      label: connectorName,
-    },
-  ];
-
-  return (
-    <Switch>
-      <Route exact path={clusterConnectorsPath(':clusterName')}>
-        <Breadcrumb>All Connectors</Breadcrumb>
-      </Route>
-      <Route exact path={clusterConnectorNewPath(':clusterName')}>
-        <Breadcrumb links={rootLinks}>New Connector</Breadcrumb>
-      </Route>
-      <Route
-        exact
-        path={clusterConnectConnectorEditPath(
-          ':clusterName',
-          ':connectName',
-          ':connectorName'
-        )}
-      >
-        <Breadcrumb links={connectorLinks}>Edit</Breadcrumb>
-      </Route>
-      <Route
-        path={clusterConnectConnectorPath(
-          ':clusterName',
-          ':connectName',
-          ':connectorName'
-        )}
-      >
-        <Breadcrumb links={rootLinks}>{connectorName}</Breadcrumb>
-      </Route>
-    </Switch>
-  );
-};
-
-export default Breadcrumbs;

+ 0 - 65
kafka-ui-react-app/src/components/Connect/Breadcrumbs/__tests__/Breadcrumbs.spec.tsx

@@ -1,65 +0,0 @@
-import React from 'react';
-import { create } from 'react-test-renderer';
-import Breadcrumbs from 'components/Connect/Breadcrumbs/Breadcrumbs';
-import {
-  clusterConnectConnectorEditPath,
-  clusterConnectConnectorPath,
-  clusterConnectorNewPath,
-  clusterConnectorsPath,
-} from 'lib/paths';
-import { TestRouterWrapper } from 'lib/testHelpers';
-
-describe('Breadcrumbs', () => {
-  const setupWrapper = (pathname: string) => (
-    <TestRouterWrapper
-      pathname={pathname}
-      urlParams={{
-        clusterName: 'my-cluster',
-        connectName: 'my-connect',
-        connectorName: 'my-connector',
-      }}
-    >
-      <Breadcrumbs />
-    </TestRouterWrapper>
-  );
-
-  it('matches snapshot for root path', () => {
-    expect(
-      create(setupWrapper(clusterConnectorsPath(':clusterName'))).toJSON()
-    ).toMatchSnapshot();
-  });
-
-  it('matches snapshot for new connector path', () => {
-    expect(
-      create(setupWrapper(clusterConnectorNewPath(':clusterName'))).toJSON()
-    ).toMatchSnapshot();
-  });
-
-  it('matches snapshot for connector edit path', () => {
-    expect(
-      create(
-        setupWrapper(
-          clusterConnectConnectorEditPath(
-            ':clusterName',
-            ':connectName',
-            ':connectorName'
-          )
-        )
-      ).toJSON()
-    ).toMatchSnapshot();
-  });
-
-  it('matches snapshot for connector path', () => {
-    expect(
-      create(
-        setupWrapper(
-          clusterConnectConnectorPath(
-            ':clusterName',
-            ':connectName',
-            ':connectorName'
-          )
-        )
-      ).toJSON()
-    ).toMatchSnapshot();
-  });
-});

+ 0 - 109
kafka-ui-react-app/src/components/Connect/Breadcrumbs/__tests__/__snapshots__/Breadcrumbs.spec.tsx.snap

@@ -1,109 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Breadcrumbs matches snapshot for connector edit path 1`] = `
-<nav
-  aria-label="breadcrumbs"
-  className="breadcrumb"
->
-  <ul>
-    <li>
-      <a
-        href="/ui/clusters/my-cluster/connectors"
-        onClick={[Function]}
-      >
-        All Connectors
-      </a>
-    </li>
-    <li>
-      <a
-        href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector"
-        onClick={[Function]}
-      >
-        my-connector
-      </a>
-    </li>
-    <li
-      className="is-active"
-    >
-      <span
-        className=""
-      >
-        Edit
-      </span>
-    </li>
-  </ul>
-</nav>
-`;
-
-exports[`Breadcrumbs matches snapshot for connector path 1`] = `
-<nav
-  aria-label="breadcrumbs"
-  className="breadcrumb"
->
-  <ul>
-    <li>
-      <a
-        href="/ui/clusters/my-cluster/connectors"
-        onClick={[Function]}
-      >
-        All Connectors
-      </a>
-    </li>
-    <li
-      className="is-active"
-    >
-      <span
-        className=""
-      >
-        my-connector
-      </span>
-    </li>
-  </ul>
-</nav>
-`;
-
-exports[`Breadcrumbs matches snapshot for new connector path 1`] = `
-<nav
-  aria-label="breadcrumbs"
-  className="breadcrumb"
->
-  <ul>
-    <li>
-      <a
-        href="/ui/clusters/my-cluster/connectors"
-        onClick={[Function]}
-      >
-        All Connectors
-      </a>
-    </li>
-    <li
-      className="is-active"
-    >
-      <span
-        className=""
-      >
-        New Connector
-      </span>
-    </li>
-  </ul>
-</nav>
-`;
-
-exports[`Breadcrumbs matches snapshot for root path 1`] = `
-<nav
-  aria-label="breadcrumbs"
-  className="breadcrumb"
->
-  <ul>
-    <li
-      className="is-active"
-    >
-      <span
-        className=""
-      >
-        All Connectors
-      </span>
-    </li>
-  </ul>
-</nav>
-`;

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

@@ -7,28 +7,13 @@ import {
   clusterConnectConnectorEditPath,
 } from 'lib/paths';
 
-import Breadcrumbs from './Breadcrumbs/Breadcrumbs';
 import ListContainer from './List/ListContainer';
 import NewContainer from './New/NewContainer';
 import DetailsContainer from './Details/DetailsContainer';
 import EditContainer from './Edit/EditContainer';
 
 const Connect: React.FC = () => (
-  <div className="section">
-    <Switch>
-      <Route
-        path={clusterConnectConnectorPath(
-          ':clusterName',
-          ':connectName',
-          ':connectorName'
-        )}
-        component={Breadcrumbs}
-      />
-      <Route
-        path={clusterConnectorsPath(':clusterName')}
-        component={Breadcrumbs}
-      />
-    </Switch>
+  <div>
     <Switch>
       <Route
         exact

+ 0 - 21
kafka-ui-react-app/src/components/Connect/ConnectorStatusTag.tsx

@@ -1,21 +0,0 @@
-import cx from 'classnames';
-import { ConnectorState } from 'generated-sources';
-import React from 'react';
-
-export interface StatusTagProps {
-  status: ConnectorState;
-}
-
-const ConnectorStatusTag: React.FC<StatusTagProps> = ({ status }) => {
-  const classNames = cx('tag', {
-    'is-success': status === ConnectorState.RUNNING,
-    'is-light': status === ConnectorState.PAUSED,
-    'is-warning': status === ConnectorState.UNASSIGNED,
-    'is-danger':
-      status === ConnectorState.FAILED || status === ConnectorState.TASK_FAILED,
-  });
-
-  return <span className={classNames}>{status}</span>;
-};
-
-export default ConnectorStatusTag;

+ 47 - 42
kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { Link, useHistory, useParams } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
 import { ConnectorState } from 'generated-sources';
 import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
 import {
@@ -7,6 +7,8 @@ import {
   clusterConnectorsPath,
 } 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;
@@ -14,6 +16,11 @@ interface RouterParams {
   connectorName: ConnectorName;
 }
 
+const ConnectorActionsWrapperStyled = styled.div`
+  display: flex;
+  gap: 8px;
+`;
+
 export interface ActionsProps {
   deleteConnector(
     clusterName: ClusterName,
@@ -78,81 +85,79 @@ const Actions: React.FC<ActionsProps> = ({
   }, [resumeConnector, clusterName, connectName, connectorName]);
 
   return (
-    <div className="buttons">
+    <ConnectorActionsWrapperStyled>
       {connectorStatus === ConnectorState.RUNNING && (
-        <button
+        <Button
+          buttonSize="M"
+          buttonType="primary"
           type="button"
-          className="button"
           onClick={pauseConnectorHandler}
           disabled={isConnectorActionRunning}
         >
-          <span className="icon">
+          <span>
             <i className="fas fa-pause" />
           </span>
           <span>Pause</span>
-        </button>
+        </Button>
       )}
 
       {connectorStatus === ConnectorState.PAUSED && (
-        <button
+        <Button
+          buttonSize="M"
+          buttonType="primary"
           type="button"
-          className="button"
           onClick={resumeConnectorHandler}
           disabled={isConnectorActionRunning}
         >
-          <span className="icon">
+          <span>
             <i className="fas fa-play" />
           </span>
           <span>Resume</span>
-        </button>
+        </Button>
       )}
 
-      <button
+      <Button
+        buttonSize="M"
+        buttonType="primary"
         type="button"
-        className="button"
         onClick={restartConnectorHandler}
         disabled={isConnectorActionRunning}
       >
-        <span className="icon">
+        <span>
           <i className="fas fa-sync-alt" />
         </span>
         <span>Restart all tasks</span>
-      </button>
-
-      {isConnectorActionRunning ? (
-        <button type="button" className="button" disabled>
-          <span className="icon">
-            <i className="fas fa-edit" />
-          </span>
-          <span>Edit config</span>
-        </button>
-      ) : (
-        <Link
-          to={clusterConnectConnectorEditPath(
-            clusterName,
-            connectName,
-            connectorName
-          )}
-          className="button"
-        >
-          <span className="icon">
-            <i className="fas fa-pencil-alt" />
-          </span>
-          <span>Edit config</span>
-        </Link>
-      )}
+      </Button>
+      <Button
+        buttonSize="M"
+        buttonType="primary"
+        type="button"
+        isLink
+        disabled={isConnectorActionRunning}
+        to={clusterConnectConnectorEditPath(
+          clusterName,
+          connectName,
+          connectorName
+        )}
+      >
+        <span>
+          <i className="fas fa-pencil-alt" />
+        </span>
+        <span>Edit config</span>
+      </Button>
 
-      <button
-        className="button is-danger"
+      <Button
+        buttonSize="M"
+        buttonType="secondary"
         type="button"
         onClick={() => setIsDeleteConnectorConfirmationVisible(true)}
         disabled={isConnectorActionRunning}
       >
-        <span className="icon">
+        <span>
           <i className="far fa-trash-alt" />
         </span>
         <span>Delete</span>
-      </button>
+      </Button>
       <ConfirmationModal
         isOpen={isDeleteConnectorConfirmationVisible}
         onCancel={() => setIsDeleteConnectorConfirmationVisible(false)}
@@ -161,7 +166,7 @@ const Actions: React.FC<ActionsProps> = ({
       >
         Are you sure you want to remove <b>{connectorName}</b> connector?
       </ConfirmationModal>
-    </div>
+    </ConnectorActionsWrapperStyled>
   );
 };
 

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

@@ -10,6 +10,8 @@ import Actions, {
 } from 'components/Connect/Details/Actions/Actions';
 import { ConnectorState } from 'generated-sources';
 import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal';
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 const mockHistoryPush = jest.fn();
 jest.mock('react-router-dom', () => ({
@@ -38,21 +40,23 @@ describe('Actions', () => {
     const connectorName = 'my-connector';
 
     const setupWrapper = (props: Partial<ActionsProps> = {}) => (
-      <TestRouterWrapper
-        pathname={pathname}
-        urlParams={{ clusterName, connectName, connectorName }}
-      >
-        <Actions
-          deleteConnector={jest.fn()}
-          isConnectorDeleting={false}
-          connectorStatus={ConnectorState.RUNNING}
-          restartConnector={jest.fn()}
-          pauseConnector={jest.fn()}
-          resumeConnector={jest.fn()}
-          isConnectorActionRunning={false}
-          {...props}
-        />
-      </TestRouterWrapper>
+      <ThemeProvider theme={theme}>
+        <TestRouterWrapper
+          pathname={pathname}
+          urlParams={{ clusterName, connectName, connectorName }}
+        >
+          <Actions
+            deleteConnector={jest.fn()}
+            isConnectorDeleting={false}
+            connectorStatus={ConnectorState.RUNNING}
+            restartConnector={jest.fn()}
+            pauseConnector={jest.fn()}
+            resumeConnector={jest.fn()}
+            isConnectorActionRunning={false}
+            {...props}
+          />
+        </TestRouterWrapper>
+      </ThemeProvider>
     );
 
     it('matches snapshot', () => {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 753 - 120
kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/__snapshots__/Actions.spec.tsx.snap


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

@@ -8,6 +8,8 @@ import {
 } from 'redux/interfaces';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import JSONEditor from 'components/common/JSONEditor/JSONEditor';
+import styled from 'styled-components';
+import { Colors } from 'theme/theme';
 
 interface RouterParams {
   clusterName: ClusterName;
@@ -26,6 +28,13 @@ export interface ConfigProps {
   config: ConnectorConfig | null;
 }
 
+const ConnectConfigWrapper = styled.div`
+  padding: 16px;
+  margin: 16px;
+  border: 1px solid ${Colors.neutral[10]};
+  border-radius: 8px;
+`;
+
 const Config: React.FC<ConfigProps> = ({
   fetchConfig,
   isConfigFetching,
@@ -44,13 +53,14 @@ const Config: React.FC<ConfigProps> = ({
   if (!config) return null;
 
   return (
-    <JSONEditor
-      readOnly
-      value={JSON.stringify(config, null, '\t')}
-      showGutter={false}
-      highlightActiveLine={false}
-      isFixedHeight
-    />
+    <ConnectConfigWrapper>
+      <JSONEditor
+        readOnly
+        value={JSON.stringify(config, null, '\t')}
+        highlightActiveLine={false}
+        isFixedHeight
+      />
+    </ConnectConfigWrapper>
   );
 };
 

+ 17 - 7
kafka-ui-react-app/src/components/Connect/Details/Config/__test__/__snapshots__/Config.spec.tsx.snap

@@ -1,18 +1,28 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Config view matches snapshot 1`] = `
-<mock-JSONEditor
-  highlightActiveLine={false}
-  isFixedHeight={true}
-  readOnly={true}
-  showGutter={false}
-  value="{
+.c0 {
+  padding: 16px;
+  margin: 16px;
+  border: 1px solid #E3E6E8;
+  border-radius: 8px;
+}
+
+<div
+  className="c0"
+>
+  <mock-JSONEditor
+    highlightActiveLine={false}
+    isFixedHeight={true}
+    readOnly={true}
+    value="{
 	\\"connector.class\\": \\"FileStreamSource\\",
 	\\"tasks.max\\": \\"10\\",
 	\\"topic\\": \\"test-topic\\",
 	\\"file\\": \\"/some/file\\"
 }"
-/>
+  />
+</div>
 `;
 
 exports[`Config view matches snapshot when fetching config 1`] = `<mock-PageLoader />`;

+ 41 - 44
kafka-ui-react-app/src/components/Connect/Details/Details.tsx

@@ -8,6 +8,8 @@ import {
   clusterConnectConnectorTasksPath,
 } from 'lib/paths';
 import PageLoader from 'components/common/PageLoader/PageLoader';
+import Navbar from 'components/common/Navigation/Navbar.styled';
+import PageHeading from 'components/common/PageHeading/PageHeading';
 
 import OverviewContainer from './Overview/OverviewContainer';
 import TasksContainer from './Tasks/TasksContainer';
@@ -61,50 +63,45 @@ const Details: React.FC<DetailsProps> = ({
   if (!connector) return null;
 
   return (
-    <div className="box">
-      <nav className="navbar mb-4" role="navigation">
-        <div className="navbar-start tabs mb-0">
-          <NavLink
-            exact
-            to={clusterConnectConnectorPath(
-              clusterName,
-              connectName,
-              connectorName
-            )}
-            className="navbar-item is-tab"
-            activeClassName="is-active"
-          >
-            Overview
-          </NavLink>
-          <NavLink
-            exact
-            to={clusterConnectConnectorTasksPath(
-              clusterName,
-              connectName,
-              connectorName
-            )}
-            className="navbar-item is-tab"
-            activeClassName="is-active"
-          >
-            Tasks
-          </NavLink>
-          <NavLink
-            exact
-            to={clusterConnectConnectorConfigPath(
-              clusterName,
-              connectName,
-              connectorName
-            )}
-            className="navbar-item is-tab"
-            activeClassName="is-active"
-          >
-            Config
-          </NavLink>
-        </div>
-        <div className="navbar-end">
-          <ActionsContainer />
-        </div>
-      </nav>
+    <div>
+      <PageHeading text={connectorName}>
+        <ActionsContainer />
+      </PageHeading>
+      <Navbar role="navigation">
+        <NavLink
+          exact
+          to={clusterConnectConnectorPath(
+            clusterName,
+            connectName,
+            connectorName
+          )}
+          activeClassName="is-active"
+        >
+          Overview
+        </NavLink>
+        <NavLink
+          exact
+          to={clusterConnectConnectorTasksPath(
+            clusterName,
+            connectName,
+            connectorName
+          )}
+          activeClassName="is-active"
+        >
+          Tasks
+        </NavLink>
+        <NavLink
+          exact
+          to={clusterConnectConnectorConfigPath(
+            clusterName,
+            connectName,
+            connectorName
+          )}
+          activeClassName="is-active"
+        >
+          Config
+        </NavLink>
+      </Navbar>
       <Switch>
         <Route
           exact

+ 26 - 37
kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import { Connector } from 'generated-sources';
-import ConnectorStatusTag from 'components/Connect/ConnectorStatusTag';
+import TagStyled from 'components/common/Tag/Tag.styled';
+import * as Metrics from 'components/common/Metrics';
 
 export interface OverviewProps {
   connector: Connector | null;
@@ -16,42 +17,30 @@ const Overview: React.FC<OverviewProps> = ({
   if (!connector) return null;
 
   return (
-    <div className="tile is-6">
-      <table className="table is-fullwidth">
-        <tbody>
-          {connector.status?.workerId && (
-            <tr>
-              <th>Worker</th>
-              <td>{connector.status.workerId}</td>
-            </tr>
-          )}
-          <tr>
-            <th>Type</th>
-            <td>{connector.type}</td>
-          </tr>
-          {connector.config['connector.class'] && (
-            <tr>
-              <th>Class</th>
-              <td>{connector.config['connector.class']}</td>
-            </tr>
-          )}
-          <tr>
-            <th>State</th>
-            <td>
-              <ConnectorStatusTag status={connector.status.state} />
-            </td>
-          </tr>
-          <tr>
-            <th>Tasks Running</th>
-            <td>{runningTasksCount}</td>
-          </tr>
-          <tr>
-            <th>Tasks Failed</th>
-            <td>{failedTasksCount}</td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
+    <Metrics.Wrapper>
+      <Metrics.Section>
+        {connector.status?.workerId && (
+          <Metrics.Indicator label="Worker">
+            {connector.status.workerId}
+          </Metrics.Indicator>
+        )}
+        <Metrics.Indicator label="Type">{connector.type}</Metrics.Indicator>
+        {connector.config['connector.class'] && (
+          <Metrics.Indicator label="Class">
+            {connector.config['connector.class']}
+          </Metrics.Indicator>
+        )}
+        <Metrics.Indicator label="State">
+          <TagStyled color="yellow">{connector.status.state}</TagStyled>
+        </Metrics.Indicator>
+        <Metrics.Indicator label="Tasks running">
+          {runningTasksCount}
+        </Metrics.Indicator>
+        <Metrics.Indicator label="Tasks failed" isAlert>
+          {failedTasksCount}
+        </Metrics.Indicator>
+      </Metrics.Section>
+    </Metrics.Wrapper>
   );
 };
 

+ 11 - 9
kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx

@@ -7,20 +7,22 @@ import Overview, {
   OverviewProps,
 } from 'components/Connect/Details/Overview/Overview';
 import { connector } from 'redux/reducers/connect/__test__/fixtures';
-
-jest.mock('components/Connect/StatusTag', () => 'mock-StatusTag');
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 describe('Overview', () => {
   containerRendersView(<OverviewContainer />, Overview);
 
   describe('view', () => {
     const setupWrapper = (props: Partial<OverviewProps> = {}) => (
-      <Overview
-        connector={connector}
-        runningTasksCount={10}
-        failedTasksCount={2}
-        {...props}
-      />
+      <ThemeProvider theme={theme}>
+        <Overview
+          connector={connector}
+          runningTasksCount={10}
+          failedTasksCount={2}
+          {...props}
+        />
+      </ThemeProvider>
     );
 
     it('matches snapshot', () => {
@@ -30,7 +32,7 @@ describe('Overview', () => {
 
     it('is empty when no connector', () => {
       const wrapper = mount(setupWrapper({ connector: null }));
-      expect(wrapper.html()).toBeNull();
+      expect(wrapper.html()).toEqual('');
     });
   });
 });

+ 215 - 57
kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/__snapshots__/Overview.spec.tsx.snap

@@ -1,66 +1,224 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Overview view matches snapshot 1`] = `
+.c5 {
+  border: none;
+  border-radius: 16px;
+  height: 20px;
+  line-height: 20px;
+  background-color: #FFEECC;
+  color: #171A1C;
+  font-size: 12px;
+  display: inline-block;
+  padding-left: 0.75em;
+  padding-right: 0.75em;
+  text-align: center;
+}
+
+.c0 {
+  padding: 1.5rem 1rem;
+  background: #F1F2F3;
+  margin-bottom: 0.5rem !important;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  gap: 16px;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+}
+
+.c3 {
+  background-color: #FFFFFF;
+  height: 68px;
+  width: -webkit-fit-content;
+  width: -moz-fit-content;
+  width: fit-content;
+  min-width: 150px;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+  -webkit-align-items: flex-start;
+  -webkit-box-align: flex-start;
+  -ms-flex-align: flex-start;
+  align-items: flex-start;
+  padding: 12px 16px;
+  box-shadow: 3px 3px 3px rgba(0,0,0,0.08);
+  margin: 0 0 3px 0;
+  -webkit-box-flex: 1;
+  -webkit-flex-grow: 1;
+  -ms-flex-positive: 1;
+  flex-grow: 1;
+}
+
+.c4 {
+  font-weight: 500;
+  font-size: 12px;
+  color: #73848C;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  gap: 10px;
+}
+
+.c1 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  gap: 2px;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+}
+
+.c1 > .c2:first-child {
+  border-top-left-radius: 8px;
+  border-bottom-left-radius: 8px;
+}
+
+.c1 > .c2:last-child {
+  border-top-right-radius: 8px;
+  border-bottom-right-radius: 8px;
+}
+
+@media screen and (max-width:1023px) {
+  .c1 > .c2:first-child,
+  .c1 > .c2:last-child {
+    border-radius: 0;
+  }
+}
+
 <div
-  className="tile is-6"
+  className="c0"
 >
-  <table
-    className="table is-fullwidth"
-  >
-    <tbody>
-      <tr>
-        <th>
-          Worker
-        </th>
-        <td>
-          kafka-connect0:8083
-        </td>
-      </tr>
-      <tr>
-        <th>
-          Type
-        </th>
-        <td>
-          SOURCE
-        </td>
-      </tr>
-      <tr>
-        <th>
-          Class
-        </th>
-        <td>
-          FileStreamSource
-        </td>
-      </tr>
-      <tr>
-        <th>
-          State
-        </th>
-        <td>
-          <span
-            className="tag is-success"
+  <div>
+    <div
+      className="c1"
+    >
+      <div
+        className="c2 c3"
+      >
+        <div>
+          <div
+            className="c4"
+          >
+            Worker
+             
+          </div>
+          <span>
+            kafka-connect0:8083
+          </span>
+        </div>
+      </div>
+      <div
+        className="c2 c3"
+      >
+        <div>
+          <div
+            className="c4"
+          >
+            Type
+             
+          </div>
+          <span>
+            SOURCE
+          </span>
+        </div>
+      </div>
+      <div
+        className="c2 c3"
+      >
+        <div>
+          <div
+            className="c4"
+          >
+            Class
+             
+          </div>
+          <span>
+            FileStreamSource
+          </span>
+        </div>
+      </div>
+      <div
+        className="c2 c3"
+      >
+        <div>
+          <div
+            className="c4"
+          >
+            State
+             
+          </div>
+          <span>
+            <p
+              className="c5"
+            >
+              RUNNING
+            </p>
+          </span>
+        </div>
+      </div>
+      <div
+        className="c2 c3"
+      >
+        <div>
+          <div
+            className="c4"
+          >
+            Tasks running
+             
+          </div>
+          <span>
+            10
+          </span>
+        </div>
+      </div>
+      <div
+        className="c2 c3"
+      >
+        <div>
+          <div
+            className="c4"
           >
-            RUNNING
+            Tasks failed
+             
+            <svg
+              fill="none"
+              height="4"
+              viewBox="0 0 4 4"
+              width="4"
+              xmlns="http://www.w3.org/2000/svg"
+            >
+              <circle
+                cx="2"
+                cy="2"
+                fill="#E61A1A"
+                r="2"
+              />
+            </svg>
+          </div>
+          <span>
+            2
           </span>
-        </td>
-      </tr>
-      <tr>
-        <th>
-          Tasks Running
-        </th>
-        <td>
-          10
-        </td>
-      </tr>
-      <tr>
-        <th>
-          Tasks Failed
-        </th>
-        <td>
-          2
-        </td>
-      </tr>
-    </tbody>
-  </table>
+        </div>
+      </div>
+    </div>
+  </div>
 </div>
 `;

+ 15 - 18
kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx

@@ -2,7 +2,10 @@ import React from 'react';
 import { useParams } from 'react-router-dom';
 import { Task, TaskId } from 'generated-sources';
 import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
-import StatusTag from 'components/Connect/StatusTag';
+import Dropdown from 'components/common/Dropdown/Dropdown';
+import DropdownItem from 'components/common/Dropdown/DropdownItem';
+import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
+import TagStyled from 'components/common/Tag/Tag.styled';
 
 interface RouterParams {
   clusterName: ClusterName;
@@ -22,33 +25,27 @@ export interface ListItemProps {
 
 const ListItem: React.FC<ListItemProps> = ({ task, restartTask }) => {
   const { clusterName, connectName, connectorName } = useParams<RouterParams>();
-  const [restarting, setRestarting] = React.useState(false);
 
   const restartTaskHandler = React.useCallback(async () => {
-    setRestarting(true);
     await restartTask(clusterName, connectName, connectorName, task.id?.task);
-    setRestarting(false);
   }, [restartTask, clusterName, connectName, connectorName, task.id?.task]);
 
   return (
     <tr>
-      <td className="has-text-overflow-ellipsis">{task.status?.id}</td>
+      <td>{task.status?.id}</td>
       <td>{task.status?.workerId}</td>
       <td>
-        <StatusTag status={task.status.state} />
+        <TagStyled color="yellow">{task.status.state}</TagStyled>
       </td>
-      <td>{task.status.trace}</td>
-      <td>
-        <button
-          type="button"
-          className="button is-small is-pulled-right"
-          onClick={restartTaskHandler}
-          disabled={restarting}
-        >
-          <span className="icon">
-            <i className="fas fa-sync-alt" />
-          </span>
-        </button>
+      <td>{task.status.trace || 'null'}</td>
+      <td style={{ width: '5%' }}>
+        <div>
+          <Dropdown label={<VerticalElipsisIcon />} right>
+            <DropdownItem onClick={restartTaskHandler}>
+              <span>Clear Messages</span>
+            </DropdownItem>
+          </Dropdown>
+        </div>
       </td>
     </tr>
   );

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

@@ -9,8 +9,8 @@ import ListItem, {
   ListItemProps,
 } from 'components/Connect/Details/Tasks/ListItem/ListItem';
 import { tasks } from 'redux/reducers/connect/__test__/fixtures';
-
-jest.mock('components/Connect/StatusTag', () => 'mock-StatusTag');
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 describe('ListItem', () => {
   containerRendersView(
@@ -33,16 +33,18 @@ describe('ListItem', () => {
     const connectorName = 'my-connector';
 
     const setupWrapper = (props: Partial<ListItemProps> = {}) => (
-      <TestRouterWrapper
-        pathname={pathname}
-        urlParams={{ clusterName, connectName, connectorName }}
-      >
-        <table>
-          <tbody>
-            <ListItem task={tasks[0]} restartTask={jest.fn()} {...props} />
-          </tbody>
-        </table>
-      </TestRouterWrapper>
+      <ThemeProvider theme={theme}>
+        <TestRouterWrapper
+          pathname={pathname}
+          urlParams={{ clusterName, connectName, connectorName }}
+        >
+          <table>
+            <tbody>
+              <ListItem task={tasks[0]} restartTask={jest.fn()} {...props} />
+            </tbody>
+          </table>
+        </TestRouterWrapper>
+      </ThemeProvider>
     );
 
     it('matches snapshot', () => {
@@ -54,7 +56,10 @@ describe('ListItem', () => {
       const restartTask = jest.fn();
       const wrapper = mount(setupWrapper({ restartTask }));
       await act(async () => {
-        wrapper.find('button').simulate('click');
+        wrapper.find('svg').simulate('click');
+      });
+      await act(async () => {
+        wrapper.find('span').simulate('click');
       });
       expect(restartTask).toHaveBeenCalledTimes(1);
       expect(restartTask).toHaveBeenCalledWith(

+ 110 - 20
kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/__snapshots__/ListItem.spec.tsx.snap

@@ -1,38 +1,128 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ListItem view matches snapshot 1`] = `
+.c2 {
+  background: transparent;
+  border: none;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: 'center';
+  -webkit-box-align: 'center';
+  -ms-flex-align: 'center';
+  align-items: 'center';
+  -webkit-box-pack: 'center';
+  -webkit-justify-content: 'center';
+  -ms-flex-pack: 'center';
+  justify-content: 'center';
+}
+
+.c2:hover {
+  cursor: pointer;
+}
+
+.c1 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-self: center;
+  -ms-flex-item-align: center;
+  align-self: center;
+}
+
+.c0 {
+  border: none;
+  border-radius: 16px;
+  height: 20px;
+  line-height: 20px;
+  background-color: #FFEECC;
+  color: #171A1C;
+  font-size: 12px;
+  display: inline-block;
+  padding-left: 0.75em;
+  padding-right: 0.75em;
+  text-align: center;
+}
+
 <table>
   <tbody>
     <tr>
-      <td
-        className="has-text-overflow-ellipsis"
-      >
+      <td>
         1
       </td>
       <td>
         kafka-connect0:8083
       </td>
       <td>
-        <mock-StatusTag
-          status="RUNNING"
-        />
+        <p
+          className="c0"
+        >
+          RUNNING
+        </p>
       </td>
-      <td />
       <td>
-        <button
-          className="button is-small is-pulled-right"
-          disabled={false}
-          onClick={[Function]}
-          type="button"
-        >
-          <span
-            className="icon"
+        null
+      </td>
+      <td
+        style={
+          Object {
+            "width": "5%",
+          }
+        }
+      >
+        <div>
+          <div
+            className="dropdown is-right"
           >
-            <i
-              className="fas fa-sync-alt"
-            />
-          </span>
-        </button>
+            <div
+              className="c1"
+            >
+              <button
+                aria-controls="dropdown-menu"
+                aria-haspopup="true"
+                className="c2"
+                onClick={[Function]}
+                type="button"
+              >
+                <svg
+                  fill="none"
+                  height="16"
+                  viewBox="0 0 4 16"
+                  width="4"
+                  xmlns="http://www.w3.org/2000/svg"
+                >
+                  <path
+                    d="M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z"
+                    fill="#73848C"
+                  />
+                </svg>
+              </button>
+            </div>
+            <div
+              className="dropdown-menu"
+              id="dropdown-menu"
+              role="menu"
+            >
+              <div
+                className="dropdown-content has-text-left"
+              >
+                <a
+                  className="dropdown-item is-link"
+                  href="#end"
+                  onClick={[Function]}
+                  role="menuitem"
+                  type="button"
+                >
+                  <span>
+                    Clear Messages
+                  </span>
+                </a>
+              </div>
+            </div>
+          </div>
+        </div>
       </td>
     </tr>
   </tbody>

+ 9 - 9
kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx

@@ -3,6 +3,8 @@ import { useParams } from 'react-router';
 import { Task } from 'generated-sources';
 import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
 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 ListItemContainer from './ListItem/ListItemContainer';
 
@@ -39,16 +41,14 @@ const Tasks: React.FC<TasksProps> = ({
   }
 
   return (
-    <table className="table is-fullwidth">
+    <Table isFullwidth>
       <thead>
         <tr>
-          <th>ID</th>
-          <th>Worker</th>
-          <th>State</th>
-          <th>Trace</th>
-          <th>
-            <span className="is-pulled-right">Restart</span>
-          </th>
+          <TableHeaderCell title="ID" />
+          <TableHeaderCell title="Worker" />
+          <TableHeaderCell title="State" />
+          <TableHeaderCell title="Trace" />
+          <TableHeaderCell />
         </tr>
       </thead>
       <tbody>
@@ -61,7 +61,7 @@ const Tasks: React.FC<TasksProps> = ({
           <ListItemContainer key={task.status?.id} task={task} />
         ))}
       </tbody>
-    </table>
+    </Table>
   );
 };
 

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

@@ -6,6 +6,8 @@ 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 { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
 
@@ -28,17 +30,19 @@ describe('Tasks', () => {
     const connectorName = 'my-connector';
 
     const setupWrapper = (props: Partial<TasksProps> = {}) => (
-      <TestRouterWrapper
-        pathname={pathname}
-        urlParams={{ clusterName, connectName, connectorName }}
-      >
-        <Tasks
-          fetchTasks={jest.fn()}
-          areTasksFetching={false}
-          tasks={tasks}
-          {...props}
-        />
-      </TestRouterWrapper>
+      <ThemeProvider theme={theme}>
+        <TestRouterWrapper
+          pathname={pathname}
+          urlParams={{ clusterName, connectName, connectorName }}
+        >
+          <Tasks
+            fetchTasks={jest.fn()}
+            areTasksFetching={false}
+            tasks={tasks}
+            {...props}
+          />
+        </TestRouterWrapper>
+      </ThemeProvider>
     );
 
     it('matches snapshot', () => {

+ 200 - 26
kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/__snapshots__/Tasks.spec.tsx.snap

@@ -1,30 +1,117 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Tasks view matches snapshot 1`] = `
+.c0 {
+  width: 100%;
+}
+
+.c0 td {
+  border-top: 1px #f1f2f3 solid;
+  font-size: 14px;
+  font-weight: 400;
+  padding: 8px 8px 8px 24px;
+  color: #171A1C;
+  vertical-align: middle;
+}
+
+.c0 tbody > tr:hover {
+  background-color: #F1F2F3;
+}
+
+.c1 {
+  padding: 4px 0 4px 24px !important;
+  border-bottom-width: 1px !important;
+  vertical-align: middle !important;
+}
+
+.c1.is-clickable {
+  cursor: pointer !important;
+  pointer-events: all !important;
+}
+
+.c1.has-text-link-dark span {
+  color: #4F4FFF !important;
+}
+
+.c1 span {
+  font-family: Inter,sans-serif;
+  font-size: 12px;
+  font-style: normal;
+  font-weight: 400;
+  line-height: 16px;
+  -webkit-letter-spacing: 0em;
+  -moz-letter-spacing: 0em;
+  -ms-letter-spacing: 0em;
+  letter-spacing: 0em;
+  text-align: left;
+  background: #FFFFFF;
+  color: #73848C;
+}
+
+.c1 span.preview {
+  margin-left: 8px;
+  font-size: 14px;
+  color: #4F4FFF;
+  cursor: pointer;
+}
+
+.c1 span.is-clickable {
+  cursor: pointer !important;
+  pointer-events: all !important;
+}
+
 <table
-  className="table is-fullwidth"
+  className="c0"
 >
   <thead>
     <tr>
-      <th>
-        ID
-      </th>
-      <th>
-        Worker
+      <th
+        className="c1"
+        title="ID"
+      >
+        <span
+          className="title"
+        >
+          ID
+        </span>
       </th>
-      <th>
-        State
+      <th
+        className="c1"
+        title="Worker"
+      >
+        <span
+          className="title"
+        >
+          Worker
+        </span>
       </th>
-      <th>
-        Trace
+      <th
+        className="c1"
+        title="State"
+      >
+        <span
+          className="title"
+        >
+          State
+        </span>
       </th>
-      <th>
+      <th
+        className="c1"
+        title="Trace"
+      >
         <span
-          className="is-pulled-right"
+          className="title"
         >
-          Restart
+          Trace
         </span>
       </th>
+      <th
+        className="c1"
+      >
+        <span
+          className="title"
+        />
+      </th>
     </tr>
   </thead>
   <tbody>
@@ -99,30 +186,117 @@ exports[`Tasks view matches snapshot 1`] = `
 exports[`Tasks view matches snapshot when fetching tasks 1`] = `<mock-PageLoader />`;
 
 exports[`Tasks view matches snapshot when no tasks 1`] = `
+.c0 {
+  width: 100%;
+}
+
+.c0 td {
+  border-top: 1px #f1f2f3 solid;
+  font-size: 14px;
+  font-weight: 400;
+  padding: 8px 8px 8px 24px;
+  color: #171A1C;
+  vertical-align: middle;
+}
+
+.c0 tbody > tr:hover {
+  background-color: #F1F2F3;
+}
+
+.c1 {
+  padding: 4px 0 4px 24px !important;
+  border-bottom-width: 1px !important;
+  vertical-align: middle !important;
+}
+
+.c1.is-clickable {
+  cursor: pointer !important;
+  pointer-events: all !important;
+}
+
+.c1.has-text-link-dark span {
+  color: #4F4FFF !important;
+}
+
+.c1 span {
+  font-family: Inter,sans-serif;
+  font-size: 12px;
+  font-style: normal;
+  font-weight: 400;
+  line-height: 16px;
+  -webkit-letter-spacing: 0em;
+  -moz-letter-spacing: 0em;
+  -ms-letter-spacing: 0em;
+  letter-spacing: 0em;
+  text-align: left;
+  background: #FFFFFF;
+  color: #73848C;
+}
+
+.c1 span.preview {
+  margin-left: 8px;
+  font-size: 14px;
+  color: #4F4FFF;
+  cursor: pointer;
+}
+
+.c1 span.is-clickable {
+  cursor: pointer !important;
+  pointer-events: all !important;
+}
+
 <table
-  className="table is-fullwidth"
+  className="c0"
 >
   <thead>
     <tr>
-      <th>
-        ID
-      </th>
-      <th>
-        Worker
+      <th
+        className="c1"
+        title="ID"
+      >
+        <span
+          className="title"
+        >
+          ID
+        </span>
       </th>
-      <th>
-        State
+      <th
+        className="c1"
+        title="Worker"
+      >
+        <span
+          className="title"
+        >
+          Worker
+        </span>
       </th>
-      <th>
-        Trace
+      <th
+        className="c1"
+        title="State"
+      >
+        <span
+          className="title"
+        >
+          State
+        </span>
       </th>
-      <th>
+      <th
+        className="c1"
+        title="Trace"
+      >
         <span
-          className="is-pulled-right"
+          className="title"
         >
-          Restart
+          Trace
         </span>
       </th>
+      <th
+        className="c1"
+      >
+        <span
+          className="title"
+        />
+      </th>
     </tr>
   </thead>
   <tbody>

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

@@ -6,6 +6,8 @@ import { clusterConnectConnectorPath } from 'lib/paths';
 import DetailsContainer from 'components/Connect/Details/DetailsContainer';
 import Details, { DetailsProps } from 'components/Connect/Details/Details';
 import { connector, tasks } from 'redux/reducers/connect/__test__/fixtures';
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
 
@@ -43,20 +45,22 @@ describe('Details', () => {
     const connectorName = 'my-connector';
 
     const setupWrapper = (props: Partial<DetailsProps> = {}) => (
-      <TestRouterWrapper
-        pathname={pathname}
-        urlParams={{ clusterName, connectName, connectorName }}
-      >
-        <Details
-          fetchConnector={jest.fn()}
-          fetchTasks={jest.fn()}
-          isConnectorFetching={false}
-          areTasksFetching={false}
-          connector={connector}
-          tasks={tasks}
-          {...props}
-        />
-      </TestRouterWrapper>
+      <ThemeProvider theme={theme}>
+        <TestRouterWrapper
+          pathname={pathname}
+          urlParams={{ clusterName, connectName, connectorName }}
+        >
+          <Details
+            fetchConnector={jest.fn()}
+            fetchTasks={jest.fn()}
+            isConnectorFetching={false}
+            areTasksFetching={false}
+            connector={connector}
+            tasks={tasks}
+            {...props}
+          />
+        </TestRouterWrapper>
+      </ThemeProvider>
     );
 
     it('matches snapshot', () => {

+ 104 - 36
kafka-ui-react-app/src/components/Connect/Details/__tests__/__snapshots__/Details.spec.tsx.snap

@@ -1,47 +1,115 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Details view matches snapshot 1`] = `
-<div
-  className="box"
->
+.c1 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  border-bottom: 1px #E3E6E8 solid;
+}
+
+.c1 a {
+  height: 40px;
+  width: 96px;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  font-weight: 500;
+  font-size: 14px;
+  color: #73848C;
+  border-bottom: 1px transparent solid;
+}
+
+.c1 a.is-active {
+  border-bottom: 1px #4F4FFF solid;
+  color: #171A1C;
+}
+
+.c1 a:hover:not(.is-active) {
+  border-bottom: 1px transparent solid;
+  color: #171A1C;
+}
+
+.c0 {
+  height: 56px;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-pack: justify;
+  -webkit-justify-content: space-between;
+  -ms-flex-pack: justify;
+  justify-content: space-between;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  padding: 0px 16px;
+}
+
+.c0 h1 {
+  font-size: 24px;
+  font-weight: 500;
+  line-height: 32px;
+  color: #000;
+}
+
+.c0 > div {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  gap: 16px;
+}
+
+<div>
+  <div
+    className="c0"
+  >
+    <h1>
+      my-connector
+    </h1>
+    <div>
+      <mock-ActionsContainer />
+    </div>
+  </div>
   <nav
-    className="navbar mb-4"
+    className="c1"
     role="navigation"
   >
-    <div
-      className="navbar-start tabs mb-0"
+    <a
+      aria-current="page"
+      className="is-active"
+      href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector"
+      onClick={[Function]}
+      style={Object {}}
     >
-      <a
-        aria-current="page"
-        className="navbar-item is-tab is-active"
-        href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector"
-        onClick={[Function]}
-        style={Object {}}
-      >
-        Overview
-      </a>
-      <a
-        aria-current={null}
-        className="navbar-item is-tab"
-        href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/tasks"
-        onClick={[Function]}
-      >
-        Tasks
-      </a>
-      <a
-        aria-current={null}
-        className="navbar-item is-tab"
-        href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/config"
-        onClick={[Function]}
-      >
-        Config
-      </a>
-    </div>
-    <div
-      className="navbar-end"
+      Overview
+    </a>
+    <a
+      aria-current={null}
+      href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/tasks"
+      onClick={[Function]}
     >
-      <mock-ActionsContainer />
-    </div>
+      Tasks
+    </a>
+    <a
+      aria-current={null}
+      href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/config"
+      onClick={[Function]}
+    >
+      Config
+    </a>
   </nav>
   <mock-OverviewContainer
     history={

+ 20 - 0
kafka-ui-react-app/src/components/Connect/Edit/Edit.styled.ts

@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+import { Colors } from 'theme/theme';
+
+export const ConnectEditWrapperStyled = styled.div`
+  margin: 16px;
+
+  & form > *:last-child {
+    margin-top: 16px;
+  }
+`;
+
+export const ConnectEditWarningMessageStyled = styled.div`
+  height: 48px;
+  display: flex;
+  align-items: center;
+  background-color: ${Colors.yellow[10]};
+  border-radius: 8px;
+  padding: 8px;
+  margin-bottom: 16px;
+`;

+ 32 - 31
kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx

@@ -14,6 +14,12 @@ import { clusterConnectConnectorConfigPath } from 'lib/paths';
 import yup from 'lib/yupExtended';
 import JSONEditor from 'components/common/JSONEditor/JSONEditor';
 import PageLoader from 'components/common/PageLoader/PageLoader';
+import { Button } from 'components/common/Button/Button';
+
+import {
+  ConnectEditWarningMessageStyled,
+  ConnectEditWrapperStyled,
+} from './Edit.styled';
 
 const validationSchema = yup.object().shape({
   config: yup.string().required().isJsonObject(),
@@ -103,41 +109,36 @@ const Edit: React.FC<EditProps> = ({
     '"******"'
   );
   return (
-    <>
+    <ConnectEditWrapperStyled>
       {hasCredentials && (
-        <div className="notification is-danger is-light">
+        <ConnectEditWarningMessageStyled>
           Please replace ****** with the real credential values to avoid
           accidentally breaking your connector config!
-        </div>
+        </ConnectEditWarningMessageStyled>
       )}
-      <div className="box">
-        <form onSubmit={handleSubmit(onSubmit)}>
-          <div className="field">
-            <div className="control">
-              <Controller
-                control={control}
-                name="config"
-                render={({ field }) => (
-                  <JSONEditor {...field} readOnly={isSubmitting} />
-                )}
-              />
-            </div>
-            <p className="help is-danger">
-              <ErrorMessage errors={errors} name="config" />
-            </p>
-          </div>
-          <div className="field">
-            <div className="control">
-              <input
-                type="submit"
-                className="button is-primary"
-                disabled={!isValid || isSubmitting || !isDirty}
-              />
-            </div>
-          </div>
-        </form>
-      </div>
-    </>
+      <form onSubmit={handleSubmit(onSubmit)}>
+        <div>
+          <Controller
+            control={control}
+            name="config"
+            render={({ field }) => (
+              <JSONEditor {...field} readOnly={isSubmitting} />
+            )}
+          />
+        </div>
+        <div>
+          <ErrorMessage errors={errors} name="config" />
+        </div>
+        <Button
+          buttonSize="M"
+          buttonType="primary"
+          type="submit"
+          disabled={!isValid || isSubmitting || !isDirty}
+        >
+          Submit
+        </Button>
+      </form>
+    </ConnectEditWrapperStyled>
   );
 };
 

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

@@ -10,6 +10,8 @@ import {
 import EditContainer from 'components/Connect/Edit/EditContainer';
 import Edit, { EditProps } from 'components/Connect/Edit/Edit';
 import { connector } from 'redux/reducers/connect/__test__/fixtures';
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
 
@@ -37,18 +39,20 @@ describe('Edit', () => {
     const connectorName = 'my-connector';
 
     const setupWrapper = (props: Partial<EditProps> = {}) => (
-      <TestRouterWrapper
-        pathname={pathname}
-        urlParams={{ clusterName, connectName, connectorName }}
-      >
-        <Edit
-          fetchConfig={jest.fn()}
-          isConfigFetching={false}
-          config={connector.config}
-          updateConfig={jest.fn()}
-          {...props}
-        />
-      </TestRouterWrapper>
+      <ThemeProvider theme={theme}>
+        <TestRouterWrapper
+          pathname={pathname}
+          urlParams={{ clusterName, connectName, connectorName }}
+        >
+          <Edit
+            fetchConfig={jest.fn()}
+            isConfigFetching={false}
+            config={connector.config}
+            updateConfig={jest.fn()}
+            {...props}
+          />
+        </TestRouterWrapper>
+      </ThemeProvider>
     );
 
     it('matches snapshot', () => {

+ 173 - 71
kafka-ui-react-app/src/components/Connect/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap

@@ -1,105 +1,207 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Edit view matches snapshot 1`] = `
+.c1 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: row;
+  -ms-flex-direction: row;
+  flex-direction: row;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+  padding: 0px 12px;
+  border: none;
+  border-radius: 4px;
+  white-space: nowrap;
+  background: #4F4FFF;
+  color: #FFFFFF;
+  font-size: 14px;
+  height: 32px;
+}
+
+.c1:hover:enabled {
+  background: #1717CF;
+  color: #FFFFFF;
+  cursor: pointer;
+}
+
+.c1:active:enabled {
+  background: #1414B8;
+  color: #FFFFFF;
+}
+
+.c1:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.c1 a {
+  color: white;
+}
+
+.c1 i {
+  margin-right: 7px;
+}
+
+.c0 {
+  margin: 16px;
+}
+
+.c0 form > *:last-child {
+  margin-top: 16px;
+}
+
 <div
-  className="box"
+  className="c0"
 >
   <form
     onSubmit={[Function]}
   >
-    <div
-      className="field"
-    >
-      <div
-        className="control"
-      >
-        <mock-JSONEditor
-          name="config"
-          onBlur={[Function]}
-          onChange={[Function]}
-          readOnly={false}
-          value="{
+    <div>
+      <mock-JSONEditor
+        name="config"
+        onBlur={[Function]}
+        onChange={[Function]}
+        readOnly={false}
+        value="{
 	\\"connector.class\\": \\"FileStreamSource\\",
 	\\"tasks.max\\": \\"10\\",
 	\\"topic\\": \\"test-topic\\",
 	\\"file\\": \\"/some/file\\"
 }"
-        />
-      </div>
-      <p
-        className="help is-danger"
       />
     </div>
-    <div
-      className="field"
+    <div />
+    <button
+      className="c1"
+      disabled={true}
+      type="submit"
     >
-      <div
-        className="control"
-      >
-        <input
-          className="button is-primary"
-          disabled={true}
-          type="submit"
-        />
-      </div>
-    </div>
+      Submit
+    </button>
   </form>
 </div>
 `;
 
 exports[`Edit view matches snapshot when config has credentials 1`] = `
-Array [
+.c2 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: row;
+  -ms-flex-direction: row;
+  flex-direction: row;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+  padding: 0px 12px;
+  border: none;
+  border-radius: 4px;
+  white-space: nowrap;
+  background: #4F4FFF;
+  color: #FFFFFF;
+  font-size: 14px;
+  height: 32px;
+}
+
+.c2:hover:enabled {
+  background: #1717CF;
+  color: #FFFFFF;
+  cursor: pointer;
+}
+
+.c2:active:enabled {
+  background: #1414B8;
+  color: #FFFFFF;
+}
+
+.c2:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.c2 a {
+  color: white;
+}
+
+.c2 i {
+  margin-right: 7px;
+}
+
+.c0 {
+  margin: 16px;
+}
+
+.c0 form > *:last-child {
+  margin-top: 16px;
+}
+
+.c1 {
+  height: 48px;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  background-color: #FFEECC;
+  border-radius: 8px;
+  padding: 8px;
+  margin-bottom: 16px;
+}
+
+<div
+  className="c0"
+>
   <div
-    className="notification is-danger is-light"
+    className="c1"
   >
     Please replace ****** with the real credential values to avoid accidentally breaking your connector config!
-  </div>,
-  <div
-    className="box"
+  </div>
+  <form
+    onSubmit={[Function]}
   >
-    <form
-      onSubmit={[Function]}
-    >
-      <div
-        className="field"
-      >
-        <div
-          className="control"
-        >
-          <mock-JSONEditor
-            name="config"
-            onBlur={[Function]}
-            onChange={[Function]}
-            readOnly={false}
-            value="{
+    <div>
+      <mock-JSONEditor
+        name="config"
+        onBlur={[Function]}
+        onChange={[Function]}
+        readOnly={false}
+        value="{
 	\\"connector.class\\": \\"FileStreamSource\\",
 	\\"tasks.max\\": \\"10\\",
 	\\"topic\\": \\"test-topic\\",
 	\\"file\\": \\"/some/file\\",
 	\\"password\\": \\"******\\"
 }"
-          />
-        </div>
-        <p
-          className="help is-danger"
-        />
-      </div>
-      <div
-        className="field"
-      >
-        <div
-          className="control"
-        >
-          <input
-            className="button is-primary"
-            disabled={true}
-            type="submit"
-          />
-        </div>
-      </div>
-    </form>
-  </div>,
-]
+      />
+    </div>
+    <div />
+    <button
+      className="c2"
+      disabled={true}
+      type="submit"
+    >
+      Submit
+    </button>
+  </form>
+</div>
 `;
 
 exports[`Edit view matches snapshot when fetching config 1`] = `<mock-PageLoader />`;

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

@@ -1,13 +1,17 @@
 import React from 'react';
-import { Link, useParams } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
 import { Connect, FullConnectorInfo } from 'generated-sources';
 import { ClusterName, ConnectorSearch } from 'redux/interfaces';
 import { clusterConnectorNewPath } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
-import Indicator from 'components/common/Dashboard/Indicator';
-import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import Search from 'components/common/Search/Search';
+import * as Metrics from 'components/common/Metrics';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import { Button } from 'components/common/Button/Button';
+import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
+import { Table } from 'components/common/table/Table/Table.styled';
+import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 
 import ListItem from './ListItem';
 
@@ -47,50 +51,51 @@ const List: React.FC<ListProps> = ({
 
   return (
     <>
-      <MetricsWrapper>
-        <Indicator
-          className="level-left is-one-third"
-          label="Connects"
-          title="Connects"
-          fetching={areConnectsFetching}
-        >
-          {connectors.length}
-        </Indicator>
-
-        <div className="column">
-          <Search
-            handleSearch={handleSearch}
-            placeholder="Search by Connect Name, Status or Type"
-            value={search}
-          />
-        </div>
-
+      <PageHeading text="Connectors">
         {!isReadOnly && (
-          <div className="level-item level-right">
-            <Link
-              className="button is-primary"
-              to={clusterConnectorNewPath(clusterName)}
-            >
-              Create Connector
-            </Link>
-          </div>
+          <Button
+            isLink
+            buttonType="primary"
+            buttonSize="M"
+            to={clusterConnectorNewPath(clusterName)}
+          >
+            Create Connector
+          </Button>
         )}
-      </MetricsWrapper>
+      </PageHeading>
+      <Metrics.Wrapper>
+        <Metrics.Section>
+          <Metrics.Indicator
+            label="Connects"
+            title="Connects"
+            fetching={areConnectsFetching}
+          >
+            {connectors.length}
+          </Metrics.Indicator>
+        </Metrics.Section>
+      </Metrics.Wrapper>
+      <ControlPanelWrapper hasInput>
+        <Search
+          handleSearch={handleSearch}
+          placeholder="Search by Connect Name, Status or Type"
+          value={search}
+        />
+      </ControlPanelWrapper>
       {areConnectorsFetching ? (
         <PageLoader />
       ) : (
-        <div className="box">
-          <table className="table is-fullwidth">
+        <div>
+          <Table isFullwidth>
             <thead>
               <tr>
-                <th>Name</th>
-                <th>Connect</th>
-                <th>Type</th>
-                <th>Plugin</th>
-                <th>Topics</th>
-                <th>Status</th>
-                <th>Running Tasks</th>
-                <th> </th>
+                <TableHeaderCell title="Name" />
+                <TableHeaderCell title="Connect" />
+                <TableHeaderCell title="Type" />
+                <TableHeaderCell title="Plugin" />
+                <TableHeaderCell title="Topics" />
+                <TableHeaderCell title="Status" />
+                <TableHeaderCell title="Running Tasks" />
+                <TableHeaderCell> </TableHeaderCell>
               </tr>
             </thead>
             <tbody>
@@ -109,7 +114,7 @@ const List: React.FC<ListProps> = ({
                 />
               ))}
             </tbody>
-          </table>
+          </Table>
         </div>
       )}
     </>

+ 21 - 26
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import cx from 'classnames';
 import { FullConnectorInfo } from 'generated-sources';
 import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
 import { ClusterName } from 'redux/interfaces';
@@ -10,13 +9,22 @@ import Dropdown from 'components/common/Dropdown/Dropdown';
 import DropdownDivider from 'components/common/Dropdown/DropdownDivider';
 import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
-import ConnectorStatusTag from 'components/Connect/ConnectorStatusTag';
+import TagStyled from 'components/common/Tag/Tag.styled';
+import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
+import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
+import { Colors } from 'theme/theme';
+import styled from 'styled-components';
 
 export interface ListItemProps {
   clusterName: ClusterName;
   connector: FullConnectorInfo;
 }
 
+const TopicTagsWrapper = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+`;
+
 const ListItem: React.FC<ListItemProps> = ({
   clusterName,
   connector: {
@@ -50,55 +58,42 @@ const ListItem: React.FC<ListItemProps> = ({
 
   return (
     <tr>
-      <td className="has-text-overflow-ellipsis">
+      <TableKeyLink>
         <NavLink
           exact
           to={clusterConnectConnectorPath(clusterName, connect, name)}
-          activeClassName="is-active"
-          className="title is-6"
         >
           {name}
         </NavLink>
-      </td>
+      </TableKeyLink>
       <td>{connect}</td>
       <td>{type}</td>
       <td>{connectorClass}</td>
       <td>
-        <div className="is-flex is-flex-wrap-wrap">
+        <TopicTagsWrapper>
           {topics?.map((t) => (
-            <span key={t} className="tag is-info is-light mr-1 mb-1">
+            <TagStyled key={t} color="gray">
               <Link to={clusterTopicPath(clusterName, t)}>{t}</Link>
-            </span>
+            </TagStyled>
           ))}
-        </div>
+        </TopicTagsWrapper>
       </td>
-      <td>{status && <ConnectorStatusTag status={status.state} />}</td>
+      <td>{status && <TagStyled color="yellow">{status.state}</TagStyled>}</td>
       <td>
         {runningTasks && (
-          <span
-            className={cx(
-              failedTasksCount ? 'has-text-danger' : 'has-text-success'
-            )}
-          >
+          <span>
             {runningTasks} of {tasksCount}
           </span>
         )}
       </td>
       <td>
-        <div className="has-text-right">
-          <Dropdown
-            label={
-              <span className="icon">
-                <i className="fas fa-cog" />
-              </span>
-            }
-            right
-          >
+        <div>
+          <Dropdown label={<VerticalElipsisIcon />} right>
             <DropdownDivider />
             <DropdownItem
               onClick={() => setDeleteConnectorConfirmationVisible(true)}
             >
-              <span className="has-text-danger">Remove Connector</span>
+              <span style={{ color: Colors.red[50] }}>Remove Connector</span>
             </DropdownItem>
           </Dropdown>
         </div>

+ 30 - 26
kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import { mount } from 'enzyme';
 import { Provider } from 'react-redux';
 import { StaticRouter } from 'react-router-dom';
-import configureStore from 'redux/store/configureStore';
+import { store } from 'redux/store';
 import { connectors } from 'redux/reducers/connect/__test__/fixtures';
 import ClusterContext, {
   ContextProps,
@@ -10,18 +10,20 @@ import ClusterContext, {
 } from 'components/contexts/ClusterContext';
 import ListContainer from 'components/Connect/List/ListContainer';
 import List, { ListProps } from 'components/Connect/List/List';
-
-const store = configureStore();
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 describe('Connectors List', () => {
   describe('Container', () => {
     it('renders view with initial state of storage', () => {
       const wrapper = mount(
-        <Provider store={store}>
-          <StaticRouter>
-            <ListContainer />
-          </StaticRouter>
-        </Provider>
+        <ThemeProvider theme={theme}>
+          <Provider store={store}>
+            <StaticRouter>
+              <ListContainer />
+            </StaticRouter>
+          </Provider>
+        </ThemeProvider>
       );
 
       expect(wrapper.exists(List)).toBeTruthy();
@@ -36,21 +38,25 @@ describe('Connectors List', () => {
       props: Partial<ListProps> = {},
       contextValue: ContextProps = initialValue
     ) => (
-      <StaticRouter>
-        <ClusterContext.Provider value={contextValue}>
-          <List
-            areConnectorsFetching
-            areConnectsFetching
-            connectors={[]}
-            connects={[]}
-            fetchConnects={fetchConnects}
-            fetchConnectors={fetchConnectors}
-            search=""
-            setConnectorSearch={setConnectorSearch}
-            {...props}
-          />
-        </ClusterContext.Provider>
-      </StaticRouter>
+      <ThemeProvider theme={theme}>
+        <Provider store={store}>
+          <StaticRouter>
+            <ClusterContext.Provider value={contextValue}>
+              <List
+                areConnectorsFetching
+                areConnectsFetching
+                connectors={[]}
+                connects={[]}
+                fetchConnects={fetchConnects}
+                fetchConnectors={fetchConnectors}
+                search=""
+                setConnectorSearch={setConnectorSearch}
+                {...props}
+              />
+            </ClusterContext.Provider>
+          </StaticRouter>
+        </Provider>
+      </ThemeProvider>
     );
 
     it('renders PageLoader', () => {
@@ -87,9 +93,7 @@ describe('Connectors List', () => {
       const wrapper = mount(
         setupComponent({}, { ...initialValue, isReadOnly: false })
       );
-      expect(
-        wrapper.exists('.level-item.level-right > .button.is-primary')
-      ).toBeTruthy();
+      expect(wrapper.exists('button')).toBeTruthy();
     });
 
     describe('readonly cluster', () => {

+ 18 - 19
kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx

@@ -3,16 +3,17 @@ import { mount } from 'enzyme';
 import { Provider } from 'react-redux';
 import { BrowserRouter } from 'react-router-dom';
 import { connectors } from 'redux/reducers/connect/__test__/fixtures';
-import configureStore from 'redux/store/configureStore';
+import { store } from 'redux/store';
 import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
 import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal';
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
-const store = configureStore();
+const mockDeleteConnector = jest.fn(() => ({ type: 'test' }));
 
-const mockDeleteConnector = jest.fn();
 jest.mock('redux/actions', () => ({
   ...jest.requireActual('redux/actions'),
-  deleteConnector: () => mockDeleteConnector(),
+  deleteConnector: () => mockDeleteConnector,
 }));
 
 jest.mock(
@@ -23,22 +24,22 @@ jest.mock(
 describe('Connectors ListItem', () => {
   const connector = connectors[0];
   const setupWrapper = (props: Partial<ListItemProps> = {}) => (
-    <Provider store={store}>
-      <BrowserRouter>
-        <table>
-          <tbody>
-            <ListItem clusterName="local" connector={connector} {...props} />
-          </tbody>
-        </table>
-      </BrowserRouter>
-    </Provider>
+    <ThemeProvider theme={theme}>
+      <Provider store={store}>
+        <BrowserRouter>
+          <table>
+            <tbody>
+              <ListItem clusterName="local" connector={connector} {...props} />
+            </tbody>
+          </table>
+        </BrowserRouter>
+      </Provider>
+    </ThemeProvider>
   );
 
   it('renders item', () => {
     const wrapper = mount(setupWrapper());
-    expect(wrapper.find('td').at(6).find('.has-text-success').text()).toEqual(
-      '2 of 2'
-    );
+    expect(wrapper.find('td').at(6).text()).toEqual('2 of 2');
   });
 
   it('renders item with failed tasks', () => {
@@ -50,9 +51,7 @@ describe('Connectors ListItem', () => {
         },
       })
     );
-    expect(wrapper.find('td').at(6).find('.has-text-danger').text()).toEqual(
-      '1 of 2'
-    );
+    expect(wrapper.find('td').at(6).text()).toEqual('1 of 2');
   });
 
   it('does not render info about tasks if taksCount is undefined', () => {

+ 476 - 215
kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap

@@ -1,246 +1,507 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Connectors ListItem matches snapshot 1`] = `
-<Provider
-  store={
+.c5 {
+  background: transparent;
+  border: none;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: 'center';
+  -webkit-box-align: 'center';
+  -ms-flex-align: 'center';
+  align-items: 'center';
+  -webkit-box-pack: 'center';
+  -webkit-justify-content: 'center';
+  -ms-flex-pack: 'center';
+  justify-content: 'center';
+}
+
+.c5:hover {
+  cursor: pointer;
+}
+
+.c4 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-self: center;
+  -ms-flex-item-align: center;
+  align-self: center;
+}
+
+.c2 {
+  border: none;
+  border-radius: 16px;
+  height: 20px;
+  line-height: 20px;
+  background-color: #E3E6E8;
+  color: #171A1C;
+  font-size: 12px;
+  display: inline-block;
+  padding-left: 0.75em;
+  padding-right: 0.75em;
+  text-align: center;
+}
+
+.c3 {
+  border: none;
+  border-radius: 16px;
+  height: 20px;
+  line-height: 20px;
+  background-color: #FFEECC;
+  color: #171A1C;
+  font-size: 12px;
+  display: inline-block;
+  padding-left: 0.75em;
+  padding-right: 0.75em;
+  text-align: center;
+}
+
+.c0 > a {
+  color: #171A1C;
+  font-weight: 500;
+  text-overflow: ellipsis;
+}
+
+.c1 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-wrap: wrap;
+  -ms-flex-wrap: wrap;
+  flex-wrap: wrap;
+}
+
+<Component
+  theme={
     Object {
-      "@@observable": [Function],
-      "dispatch": [Function],
-      "getState": [Function],
-      "replaceReducer": [Function],
-      "subscribe": [Function],
+      "alert": Object {
+        "color": Object {
+          "error": "#FAD1D1",
+          "info": "#E3E6E8",
+          "success": "#D6F5E0",
+          "warning": "#FFEECC",
+        },
+      },
+      "buttonStyles": Object {
+        "fontSize": Object {
+          "L": "16px",
+          "M": "14px",
+          "S": "14px",
+        },
+        "height": Object {
+          "L": "40px",
+          "M": "32px",
+          "S": "24px",
+        },
+        "primary": Object {
+          "backgroundColor": Object {
+            "active": "#1414B8",
+            "hover": "#1717CF",
+            "normal": "#4F4FFF",
+          },
+          "color": "#FFFFFF",
+          "invertedColors": Object {
+            "active": "#1414B8",
+            "hover": "#1717CF",
+            "normal": "#4F4FFF",
+          },
+        },
+        "secondary": Object {
+          "backgroundColor": Object {
+            "active": "#D5DADD",
+            "hover": "#E3E6E8",
+            "normal": "#F1F2F3",
+          },
+          "color": "#171A1C",
+          "invertedColors": Object {
+            "active": "#171A1C",
+            "hover": "#454F54",
+            "normal": "#73848C",
+          },
+        },
+      },
+      "layout": Object {
+        "minWidth": "1200px",
+        "navBarHeight": "3.25rem",
+        "navBarWidth": "201px",
+      },
+      "menuStyles": Object {
+        "backgroundColor": Object {
+          "active": "#E3E6E8",
+          "hover": "#F1F2F3",
+          "normal": "#FFFFFF",
+        },
+        "chevronIconColor": "#73848C",
+        "color": Object {
+          "active": "#171A1C",
+          "hover": "#73848C",
+          "normal": "#73848C",
+        },
+        "statusIconColor": Object {
+          "offline": "#E51A1A",
+          "online": "#5CD685",
+        },
+      },
+      "metrics": Object {
+        "backgroundColor": "#F1F2F3",
+        "indicator": Object {
+          "backgroundColor": "#FFFFFF",
+          "lightTextColor": "#ABB5BA",
+          "titleColor": "#73848C",
+          "warningTextColor": "#E51A1A",
+        },
+      },
+      "pageLoader": Object {
+        "borderBottomColor": "#FFFFFF",
+        "borderColor": "#4F4FFF",
+      },
+      "paginationStyles": Object {
+        "borderColor": Object {
+          "active": "#454F54",
+          "disabled": "#C7CED1",
+          "hover": "#73848C",
+          "normal": "#ABB5BA",
+        },
+        "color": Object {
+          "active": "#171A1C",
+          "disabled": "#C7CED1",
+          "hover": "#171A1C",
+          "normal": "#171A1C",
+        },
+      },
+      "primaryTabStyles": Object {
+        "borderColor": Object {
+          "active": "#4F4FFF",
+          "hover": "transparent",
+          "normal": "transparent",
+        },
+        "color": Object {
+          "active": "#171A1C",
+          "hover": "#171A1C",
+          "normal": "#73848C",
+        },
+      },
+      "secondaryTabStyles": Object {
+        "backgroundColor": Object {
+          "active": "#E3E6E8",
+          "hover": "#F1F2F3",
+          "normal": "#FFFFFF",
+        },
+        "color": Object {
+          "active": "#171A1C",
+          "hover": "#171A1C",
+          "normal": "#73848C",
+        },
+      },
+      "selectStyles": Object {
+        "borderColor": Object {
+          "active": "#454F54",
+          "disabled": "#E3E6E8",
+          "hover": "#73848C",
+          "normal": "#ABB5BA",
+        },
+        "color": Object {
+          "active": "#171A1C",
+          "disabled": "#ABB5BA",
+          "hover": "#171A1C",
+          "normal": "#171A1C",
+        },
+      },
+      "switch": Object {
+        "checked": "#29A352",
+        "unchecked": "#ABB5BA",
+      },
+      "tagStyles": Object {
+        "backgroundColor": Object {
+          "gray": "#E3E6E8",
+          "green": "#D6F5E0",
+          "yellow": "#FFEECC",
+        },
+        "color": "#171A1C",
+      },
+      "thStyles": Object {
+        "backgroundColor": Object {
+          "normal": "#FFFFFF",
+        },
+        "color": Object {
+          "normal": "#73848C",
+        },
+        "previewColor": Object {
+          "normal": "#4F4FFF",
+        },
+      },
     }
   }
 >
-  <BrowserRouter>
-    <Router
-      history={
-        Object {
-          "action": "POP",
-          "block": [Function],
-          "createHref": [Function],
-          "go": [Function],
-          "goBack": [Function],
-          "goForward": [Function],
-          "length": 1,
-          "listen": [Function],
-          "location": Object {
-            "hash": "",
-            "pathname": "/",
-            "search": "",
-            "state": undefined,
-          },
-          "push": [Function],
-          "replace": [Function],
-        }
+  <Provider
+    store={
+      Object {
+        "@@observable": [Function],
+        "dispatch": [Function],
+        "getState": [Function],
+        "replaceReducer": [Function],
+        "subscribe": [Function],
       }
-    >
-      <table>
-        <tbody>
-          <ListItem
-            clusterName="local"
-            connector={
-              Object {
-                "connect": "first",
-                "connectorClass": "FileStreamSource",
-                "failedTasksCount": 0,
-                "name": "hdfs-source-connector",
-                "status": Object {
-                  "state": "RUNNING",
-                },
-                "tasksCount": 2,
-                "topics": Array [
-                  "test-topic",
-                ],
-                "type": "SOURCE",
+    }
+  >
+    <BrowserRouter>
+      <Router
+        history={
+          Object {
+            "action": "POP",
+            "block": [Function],
+            "createHref": [Function],
+            "go": [Function],
+            "goBack": [Function],
+            "goForward": [Function],
+            "length": 1,
+            "listen": [Function],
+            "location": Object {
+              "hash": "",
+              "pathname": "/",
+              "search": "",
+              "state": undefined,
+            },
+            "push": [Function],
+            "replace": [Function],
+          }
+        }
+      >
+        <table>
+          <tbody>
+            <ListItem
+              clusterName="local"
+              connector={
+                Object {
+                  "connect": "first",
+                  "connectorClass": "FileStreamSource",
+                  "failedTasksCount": 0,
+                  "name": "hdfs-source-connector",
+                  "status": Object {
+                    "state": "RUNNING",
+                  },
+                  "tasksCount": 2,
+                  "topics": Array [
+                    "test-topic",
+                  ],
+                  "type": "SOURCE",
+                }
               }
-            }
-          >
-            <tr>
-              <td
-                className="has-text-overflow-ellipsis"
-              >
-                <NavLink
-                  activeClassName="is-active"
-                  className="title is-6"
-                  exact={true}
-                  to="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
-                >
-                  <Link
-                    aria-current={null}
-                    className="title is-6"
-                    to={
-                      Object {
-                        "hash": "",
-                        "pathname": "/ui/clusters/local/connects/first/connectors/hdfs-source-connector",
-                        "search": "",
-                        "state": null,
-                      }
-                    }
+            >
+              <tr>
+                <styled.td>
+                  <td
+                    className="c0"
                   >
-                    <LinkAnchor
-                      aria-current={null}
-                      className="title is-6"
-                      href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
-                      navigate={[Function]}
+                    <NavLink
+                      exact={true}
+                      to="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
                     >
-                      <a
+                      <Link
                         aria-current={null}
-                        className="title is-6"
-                        href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
-                        onClick={[Function]}
+                        to={
+                          Object {
+                            "hash": "",
+                            "pathname": "/ui/clusters/local/connects/first/connectors/hdfs-source-connector",
+                            "search": "",
+                            "state": null,
+                          }
+                        }
                       >
-                        hdfs-source-connector
-                      </a>
-                    </LinkAnchor>
-                  </Link>
-                </NavLink>
-              </td>
-              <td>
-                first
-              </td>
-              <td>
-                SOURCE
-              </td>
-              <td>
-                FileStreamSource
-              </td>
-              <td>
-                <div
-                  className="is-flex is-flex-wrap-wrap"
-                >
-                  <span
-                    className="tag is-info is-light mr-1 mb-1"
-                    key="test-topic"
-                  >
-                    <Link
-                      to="/ui/clusters/local/topics/test-topic"
+                        <LinkAnchor
+                          aria-current={null}
+                          href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
+                          navigate={[Function]}
+                        >
+                          <a
+                            aria-current={null}
+                            href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
+                            onClick={[Function]}
+                          >
+                            hdfs-source-connector
+                          </a>
+                        </LinkAnchor>
+                      </Link>
+                    </NavLink>
+                  </td>
+                </styled.td>
+                <td>
+                  first
+                </td>
+                <td>
+                  SOURCE
+                </td>
+                <td>
+                  FileStreamSource
+                </td>
+                <td>
+                  <styled.div>
+                    <div
+                      className="c1"
                     >
-                      <LinkAnchor
-                        href="/ui/clusters/local/topics/test-topic"
-                        navigate={[Function]}
+                      <Styled(Tag)
+                        color="gray"
+                        key="test-topic"
                       >
-                        <a
-                          href="/ui/clusters/local/topics/test-topic"
-                          onClick={[Function]}
+                        <Tag
+                          className="c2"
+                          color="gray"
                         >
-                          test-topic
-                        </a>
-                      </LinkAnchor>
-                    </Link>
-                  </span>
-                </div>
-              </td>
-              <td>
-                <ConnectorStatusTag
-                  status="RUNNING"
-                >
-                  <span
-                    className="tag is-success"
+                          <p
+                            className="c2"
+                          >
+                            <Link
+                              to="/ui/clusters/local/topics/test-topic"
+                            >
+                              <LinkAnchor
+                                href="/ui/clusters/local/topics/test-topic"
+                                navigate={[Function]}
+                              >
+                                <a
+                                  href="/ui/clusters/local/topics/test-topic"
+                                  onClick={[Function]}
+                                >
+                                  test-topic
+                                </a>
+                              </LinkAnchor>
+                            </Link>
+                          </p>
+                        </Tag>
+                      </Styled(Tag)>
+                    </div>
+                  </styled.div>
+                </td>
+                <td>
+                  <Styled(Tag)
+                    color="yellow"
                   >
-                    RUNNING
-                  </span>
-                </ConnectorStatusTag>
-              </td>
-              <td>
-                <span
-                  className="has-text-success"
-                >
-                  2
-                   of 
-                  2
-                </span>
-              </td>
-              <td>
-                <div
-                  className="has-text-right"
-                >
-                  <Dropdown
-                    label={
-                      <span
-                        className="icon"
+                    <Tag
+                      className="c3"
+                      color="yellow"
+                    >
+                      <p
+                        className="c3"
                       >
-                        <i
-                          className="fas fa-cog"
-                        />
-                      </span>
-                    }
-                    right={true}
-                  >
-                    <div
-                      className="dropdown is-right"
+                        RUNNING
+                      </p>
+                    </Tag>
+                  </Styled(Tag)>
+                </td>
+                <td>
+                  <span>
+                    2
+                     of 
+                    2
+                  </span>
+                </td>
+                <td>
+                  <div>
+                    <Dropdown
+                      label={<VerticalElipsisIcon />}
+                      right={true}
                     >
                       <div
-                        className="dropdown-trigger"
+                        className="dropdown is-right"
                       >
-                        <button
-                          aria-controls="dropdown-menu"
-                          aria-haspopup="true"
-                          className="button is-small is-link"
-                          onClick={[Function]}
-                          type="button"
-                        >
-                          <span
-                            className="icon"
+                        <styled.div>
+                          <div
+                            className="c4"
                           >
-                            <i
-                              className="fas fa-cog"
-                            />
-                          </span>
-                        </button>
-                      </div>
-                      <div
-                        className="dropdown-menu"
-                        id="dropdown-menu"
-                        role="menu"
-                      >
+                            <Styled(DropdownTrigger)
+                              onClick={[Function]}
+                            >
+                              <DropdownTrigger
+                                className="c5"
+                                onClick={[Function]}
+                              >
+                                <button
+                                  aria-controls="dropdown-menu"
+                                  aria-haspopup="true"
+                                  className="c5"
+                                  onClick={[Function]}
+                                  type="button"
+                                >
+                                  <VerticalElipsisIcon>
+                                    <svg
+                                      fill="none"
+                                      height="16"
+                                      viewBox="0 0 4 16"
+                                      width="4"
+                                      xmlns="http://www.w3.org/2000/svg"
+                                    >
+                                      <path
+                                        d="M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z"
+                                        fill="#73848C"
+                                      />
+                                    </svg>
+                                  </VerticalElipsisIcon>
+                                </button>
+                              </DropdownTrigger>
+                            </Styled(DropdownTrigger)>
+                          </div>
+                        </styled.div>
                         <div
-                          className="dropdown-content has-text-left"
+                          className="dropdown-menu"
+                          id="dropdown-menu"
+                          role="menu"
                         >
-                          <DropdownDivider>
-                            <hr
-                              className="dropdown-divider"
-                            />
-                          </DropdownDivider>
-                          <DropdownItem
-                            onClick={[Function]}
+                          <div
+                            className="dropdown-content has-text-left"
                           >
-                            <a
-                              className="dropdown-item is-link"
-                              href="#end"
+                            <DropdownDivider>
+                              <hr
+                                className="dropdown-divider"
+                              />
+                            </DropdownDivider>
+                            <DropdownItem
                               onClick={[Function]}
-                              role="menuitem"
-                              type="button"
                             >
-                              <span
-                                className="has-text-danger"
+                              <a
+                                className="dropdown-item is-link"
+                                href="#end"
+                                onClick={[Function]}
+                                role="menuitem"
+                                type="button"
                               >
-                                Remove Connector
-                              </span>
-                            </a>
-                          </DropdownItem>
+                                <span
+                                  style={
+                                    Object {
+                                      "color": "#E51A1A",
+                                    }
+                                  }
+                                >
+                                  Remove Connector
+                                </span>
+                              </a>
+                            </DropdownItem>
+                          </div>
                         </div>
                       </div>
-                    </div>
-                  </Dropdown>
-                </div>
-                <mock-ConfirmationModal
-                  isOpen={false}
-                  onCancel={[Function]}
-                  onConfirm={[Function]}
-                >
-                  Are you sure want to remove 
-                  <b>
-                    hdfs-source-connector
-                  </b>
-                   connector?
-                </mock-ConfirmationModal>
-              </td>
-            </tr>
-          </ListItem>
-        </tbody>
-      </table>
-    </Router>
-  </BrowserRouter>
-</Provider>
+                    </Dropdown>
+                  </div>
+                  <mock-ConfirmationModal
+                    isOpen={false}
+                    onCancel={[Function]}
+                    onConfirm={[Function]}
+                  >
+                    Are you sure want to remove 
+                    <b>
+                      hdfs-source-connector
+                    </b>
+                     connector?
+                  </mock-ConfirmationModal>
+                </td>
+              </tr>
+            </ListItem>
+          </tbody>
+        </table>
+      </Router>
+    </BrowserRouter>
+  </Provider>
+</Component>
 `;

+ 72 - 60
kafka-ui-react-app/src/components/Connect/New/New.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { useHistory, useParams } from 'react-router-dom';
-import { Controller, useForm } from 'react-hook-form';
+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';
@@ -9,6 +9,13 @@ import { clusterConnectConnectorPath } from 'lib/paths';
 import yup from 'lib/yupExtended';
 import JSONEditor from 'components/common/JSONEditor/JSONEditor';
 import PageLoader from 'components/common/PageLoader/PageLoader';
+import { InputLabel } from 'components/common/Input/InputLabel.styled';
+import Select from 'components/common/Select/Select';
+import { FormError } from 'components/common/Input/Input.styled';
+import Input from 'components/common/Input/Input';
+import { Button } from 'components/common/Button/Button';
+import styled from 'styled-components';
+import PageHeading from 'components/common/PageHeading/PageHeading';
 
 const validationSchema = yup.object().shape({
   name: yup.string().required(),
@@ -19,6 +26,17 @@ interface RouterParams {
   clusterName: ClusterName;
 }
 
+const NewConnectFormStyled = styled.form`
+  padding: 16px;
+  padding-top: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  & > button:last-child {
+    align-self: flex-start;
+  }
+`;
+
 export interface NewProps {
   fetchConnects(clusterName: ClusterName): void;
   areConnectsFetching: boolean;
@@ -45,14 +63,7 @@ const New: React.FC<NewProps> = ({
   const { clusterName } = useParams<RouterParams>();
   const history = useHistory();
 
-  const {
-    register,
-    handleSubmit,
-    control,
-    formState: { isDirty, isSubmitting, isValid, errors },
-    getValues,
-    setValue,
-  } = useForm<FormValues>({
+  const methods = useForm<FormValues>({
     mode: 'onTouched',
     resolver: yupResolver(validationSchema),
     defaultValues: {
@@ -61,6 +72,13 @@ const New: React.FC<NewProps> = ({
       config: '',
     },
   });
+  const {
+    handleSubmit,
+    control,
+    formState: { isDirty, isSubmitting, isValid, errors },
+    getValues,
+    setValue,
+  } = methods;
 
   React.useEffect(() => {
     fetchConnects(clusterName);
@@ -105,66 +123,60 @@ const New: React.FC<NewProps> = ({
   }
 
   return (
-    <div className="box">
-      <form onSubmit={handleSubmit(onSubmit)}>
+    <FormProvider {...methods}>
+      <PageHeading text="Create new connector" />
+      <NewConnectFormStyled onSubmit={handleSubmit(onSubmit)}>
         <div className={['field', connectNameFieldClassName].join(' ')}>
-          <label className="label">Connect *</label>
-          <div className="control select">
-            <select {...register('connectName')} disabled={isSubmitting}>
-              {connects.map(({ name }) => (
-                <option key={name} value={name}>
-                  {name}
-                </option>
-              ))}
-            </select>
-          </div>
-          <p className="help is-danger">
+          <InputLabel>Connect *</InputLabel>
+          <Select selectSize="M" name="connectName" disabled={isSubmitting}>
+            {connects.map(({ name }) => (
+              <option key={name} value={name}>
+                {name}
+              </option>
+            ))}
+          </Select>
+          <FormError>
             <ErrorMessage errors={errors} name="connectName" />
-          </p>
+          </FormError>
         </div>
 
-        <div className="field">
-          <label className="label">Name *</label>
-          <div className="control">
-            <input
-              className="input"
-              placeholder="Connector Name"
-              {...register('name')}
-              autoComplete="off"
-              disabled={isSubmitting}
-            />
-          </div>
-          <p className="help is-danger">
+        <div>
+          <InputLabel>Name *</InputLabel>
+          <Input
+            inputSize="M"
+            placeholder="Connector Name"
+            name="name"
+            autoComplete="off"
+            disabled={isSubmitting}
+          />
+          <FormError>
             <ErrorMessage errors={errors} name="name" />
-          </p>
+          </FormError>
         </div>
 
-        <div className="field">
-          <label className="label">Config *</label>
-          <div className="control">
-            <Controller
-              control={control}
-              name="config"
-              render={({ field }) => (
-                <JSONEditor {...field} readOnly={isSubmitting} />
-              )}
-            />
-          </div>
-          <p className="help is-danger">
+        <div>
+          <InputLabel>Config *</InputLabel>
+          <Controller
+            control={control}
+            name="config"
+            render={({ field }) => (
+              <JSONEditor {...field} readOnly={isSubmitting} />
+            )}
+          />
+          <FormError>
             <ErrorMessage errors={errors} name="config" />
-          </p>
-        </div>
-        <div className="field">
-          <div className="control">
-            <input
-              type="submit"
-              className="button is-primary"
-              disabled={!isValid || isSubmitting || !isDirty}
-            />
-          </div>
+          </FormError>
         </div>
-      </form>
-    </div>
+        <Button
+          buttonSize="M"
+          buttonType="primary"
+          type="submit"
+          disabled={!isValid || isSubmitting || !isDirty}
+        >
+          Submit
+        </Button>
+      </NewConnectFormStyled>
+    </FormProvider>
   );
 };
 

+ 14 - 10
kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx

@@ -10,6 +10,8 @@ import {
 import NewContainer from 'components/Connect/New/NewContainer';
 import New, { NewProps } from 'components/Connect/New/New';
 import { connects, connector } from 'redux/reducers/connect/__test__/fixtures';
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
 
 jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
 
@@ -37,19 +39,21 @@ describe('New', () => {
         wrapper
           .find('mock-JSONEditor')
           .simulate('change', { target: { value: '{"class":"MyClass"}' } });
-        wrapper.find('input[type="submit"]').simulate('submit');
+        wrapper.find('button[type="submit"]').simulate('submit');
       });
 
     const setupWrapper = (props: Partial<NewProps> = {}) => (
-      <TestRouterWrapper pathname={pathname} urlParams={{ clusterName }}>
-        <New
-          fetchConnects={jest.fn()}
-          areConnectsFetching={false}
-          connects={connects}
-          createConnector={jest.fn()}
-          {...props}
-        />
-      </TestRouterWrapper>
+      <ThemeProvider theme={theme}>
+        <TestRouterWrapper pathname={pathname} urlParams={{ clusterName }}>
+          <New
+            fetchConnects={jest.fn()}
+            areConnectsFetching={false}
+            connects={connects}
+            createConnector={jest.fn()}
+            {...props}
+          />
+        </TestRouterWrapper>
+      </ThemeProvider>
     );
 
     it('matches snapshot', async () => {

+ 268 - 43
kafka-ui-react-app/src/components/Connect/New/__tests__/__snapshots__/New.spec.tsx.snap

@@ -1,24 +1,263 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`New view matches snapshot 1`] = `
+Array [
+  .c0 {
+  height: 56px;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-pack: justify;
+  -webkit-justify-content: space-between;
+  -ms-flex-pack: justify;
+  justify-content: space-between;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  padding: 0px 16px;
+}
+
+.c0 h1 {
+  font-size: 24px;
+  font-weight: 500;
+  line-height: 32px;
+  color: #000;
+}
+
+.c0 > div {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  gap: 16px;
+}
+
 <div
-  className="box"
->
-  <form
+    className="c0"
+  >
+    <h1>
+      Create new connector
+    </h1>
+    <div />
+  </div>,
+  .c1 {
+  font-weight: 500;
+  font-size: 12px;
+  line-height: 20px;
+  color: #454F54;
+}
+
+.c3 {
+  height: 32px;
+  border: 1px #ABB5BA solid;
+  border-radius: 4px;
+  font-size: 14px;
+  width: 100%;
+  padding-left: 12px;
+  padding-right: 16px;
+  color: #171A1C;
+  background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
+  background-repeat: no-repeat !important;
+  background-position-x: calc(100% - 8px) !important;
+  background-position-y: 55% !important;
+  -webkit-appearance: none !important;
+  -moz-appearance: none !important;
+  appearance: none !important;
+}
+
+.c3:hover {
+  color: #171A1C;
+  border-color: #73848C;
+}
+
+.c3:focus {
+  outline: none;
+  color: #171A1C;
+  border-color: #454F54;
+}
+
+.c3:disabled {
+  color: #ABB5BA;
+  border-color: #E3E6E8;
+  cursor: not-allowed;
+}
+
+.c2 {
+  position: relative;
+}
+
+.c6 {
+  border: 1px #ABB5BA solid;
+  border-radius: 4px;
+  height: 32px;
+  width: 100%;
+  padding-left: 12px;
+  font-size: 14px;
+}
+
+.c6::-webkit-input-placeholder {
+  color: #ABB5BA;
+  font-size: 14px;
+}
+
+.c6::-moz-placeholder {
+  color: #ABB5BA;
+  font-size: 14px;
+}
+
+.c6:-ms-input-placeholder {
+  color: #ABB5BA;
+  font-size: 14px;
+}
+
+.c6::placeholder {
+  color: #ABB5BA;
+  font-size: 14px;
+}
+
+.c6:hover {
+  border-color: #73848C;
+}
+
+.c6:focus {
+  outline: none;
+  border-color: #454F54;
+}
+
+.c6:focus::-webkit-input-placeholder {
+  color: transparent;
+}
+
+.c6:focus::-moz-placeholder {
+  color: transparent;
+}
+
+.c6:focus:-ms-input-placeholder {
+  color: transparent;
+}
+
+.c6:focus::placeholder {
+  color: transparent;
+}
+
+.c6:disabled {
+  color: #ABB5BA;
+  border-color: #E3E6E8;
+  cursor: not-allowed;
+}
+
+.c6:read-only {
+  color: #171A1C;
+  border: none;
+  background-color: #F1F2F3;
+  cursor: not-allowed;
+}
+
+.c6:-moz-read-only:focus::placeholder {
+  color: #ABB5BA;
+}
+
+.c6:read-only:focus::placeholder {
+  color: #ABB5BA;
+}
+
+.c4 {
+  color: #E51A1A;
+  font-size: 12px;
+}
+
+.c5 {
+  position: relative;
+}
+
+.c7 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: row;
+  -ms-flex-direction: row;
+  flex-direction: row;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+  padding: 0px 12px;
+  border: none;
+  border-radius: 4px;
+  white-space: nowrap;
+  background: #4F4FFF;
+  color: #FFFFFF;
+  font-size: 14px;
+  height: 32px;
+}
+
+.c7:hover:enabled {
+  background: #1717CF;
+  color: #FFFFFF;
+  cursor: pointer;
+}
+
+.c7:active:enabled {
+  background: #1414B8;
+  color: #FFFFFF;
+}
+
+.c7:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.c7 a {
+  color: white;
+}
+
+.c7 i {
+  margin-right: 7px;
+}
+
+.c0 {
+  padding: 16px;
+  padding-top: 0;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.c0 > button:last-child {
+  -webkit-align-self: flex-start;
+  -ms-flex-item-align: start;
+  align-self: flex-start;
+}
+
+<form
+    className="c0"
     onSubmit={[Function]}
   >
     <div
       className="field "
     >
       <label
-        className="label"
+        className="c1"
       >
         Connect *
       </label>
       <div
-        className="control select"
+        className="select-wrapper c2"
       >
         <select
+          className="c3"
           disabled={false}
           name="connectName"
           onBlur={[Function]}
@@ -37,23 +276,21 @@ exports[`New view matches snapshot 1`] = `
         </select>
       </div>
       <p
-        className="help is-danger"
+        className="c4"
       />
     </div>
-    <div
-      className="field"
-    >
+    <div>
       <label
-        className="label"
+        className="c1"
       >
         Name *
       </label>
       <div
-        className="control"
+        className="c5"
       >
         <input
           autoComplete="off"
-          className="input"
+          className="c6 c5"
           disabled={false}
           name="name"
           onBlur={[Function]}
@@ -62,47 +299,35 @@ exports[`New view matches snapshot 1`] = `
         />
       </div>
       <p
-        className="help is-danger"
+        className="c4"
       />
     </div>
-    <div
-      className="field"
-    >
+    <div>
       <label
-        className="label"
+        className="c1"
       >
         Config *
       </label>
-      <div
-        className="control"
-      >
-        <mock-JSONEditor
-          name="config"
-          onBlur={[Function]}
-          onChange={[Function]}
-          readOnly={false}
-          value=""
-        />
-      </div>
+      <mock-JSONEditor
+        name="config"
+        onBlur={[Function]}
+        onChange={[Function]}
+        readOnly={false}
+        value=""
+      />
       <p
-        className="help is-danger"
+        className="c4"
       />
     </div>
-    <div
-      className="field"
+    <button
+      className="c7"
+      disabled={true}
+      type="submit"
     >
-      <div
-        className="control"
-      >
-        <input
-          className="button is-primary"
-          disabled={true}
-          type="submit"
-        />
-      </div>
-    </div>
-  </form>
-</div>
+      Submit
+    </button>
+  </form>,
+]
 `;
 
 exports[`New view matches snapshot when fetching connects 1`] = `<mock-PageLoader />`;

+ 0 - 20
kafka-ui-react-app/src/components/Connect/StatusTag.tsx

@@ -1,20 +0,0 @@
-import cx from 'classnames';
-import { ConnectorTaskStatus } from 'generated-sources';
-import React from 'react';
-
-export interface StatusTagProps {
-  status: ConnectorTaskStatus;
-}
-
-const StatusTag: React.FC<StatusTagProps> = ({ status }) => {
-  const classNames = cx('tag', {
-    'is-success': status === ConnectorTaskStatus.RUNNING,
-    'is-light': status === ConnectorTaskStatus.PAUSED,
-    'is-warning': status === ConnectorTaskStatus.UNASSIGNED,
-    'is-danger': status === ConnectorTaskStatus.FAILED,
-  });
-
-  return <span className={classNames}>{status}</span>;
-};
-
-export default StatusTag;

+ 0 - 38
kafka-ui-react-app/src/components/Connect/__tests__/StatusTag.spec.tsx

@@ -1,38 +0,0 @@
-import React from 'react';
-import { create } from 'react-test-renderer';
-import StatusTag, { StatusTagProps } from 'components/Connect/StatusTag';
-import { ConnectorTaskStatus } from 'generated-sources';
-
-describe('StatusTag', () => {
-  const setupWrapper = (props: Partial<StatusTagProps> = {}) => (
-    <StatusTag status={ConnectorTaskStatus.RUNNING} {...props} />
-  );
-
-  it('matches snapshot for running status', () => {
-    const wrapper = create(
-      setupWrapper({ status: ConnectorTaskStatus.RUNNING })
-    );
-    expect(wrapper.toJSON()).toMatchSnapshot();
-  });
-
-  it('matches snapshot for failed status', () => {
-    const wrapper = create(
-      setupWrapper({ status: ConnectorTaskStatus.FAILED })
-    );
-    expect(wrapper.toJSON()).toMatchSnapshot();
-  });
-
-  it('matches snapshot for paused status', () => {
-    const wrapper = create(
-      setupWrapper({ status: ConnectorTaskStatus.PAUSED })
-    );
-    expect(wrapper.toJSON()).toMatchSnapshot();
-  });
-
-  it('matches snapshot for unassigned status', () => {
-    const wrapper = create(
-      setupWrapper({ status: ConnectorTaskStatus.UNASSIGNED })
-    );
-    expect(wrapper.toJSON()).toMatchSnapshot();
-  });
-});

+ 2 - 14
kafka-ui-react-app/src/components/Connect/__tests__/__snapshots__/Connect.spec.tsx.snap

@@ -1,19 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Connect matches snapshot 1`] = `
-<div
-  className="section"
->
-  <Switch>
-    <Route
-      component={[Function]}
-      path="/ui/clusters/:clusterName/connects/:connectName/connectors/:connectorName"
-    />
-    <Route
-      component={[Function]}
-      path="/ui/clusters/:clusterName/connectors"
-    />
-  </Switch>
+<div>
   <Switch>
     <Route
       component={
@@ -30,7 +18,7 @@ exports[`Connect matches snapshot 1`] = `
     <Route
       component={[Function]}
       exact={true}
-      path="/ui/clusters/:clusterName/connectors/create_new"
+      path="/ui/clusters/:clusterName/connectors/create-new"
     />
     <Route
       component={[Function]}

+ 0 - 33
kafka-ui-react-app/src/components/Connect/__tests__/__snapshots__/StatusTag.spec.tsx.snap

@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`StatusTag matches snapshot for failed status 1`] = `
-<span
-  className="tag is-danger"
->
-  FAILED
-</span>
-`;
-
-exports[`StatusTag matches snapshot for paused status 1`] = `
-<span
-  className="tag is-light"
->
-  PAUSED
-</span>
-`;
-
-exports[`StatusTag matches snapshot for running status 1`] = `
-<span
-  className="tag is-success"
->
-  RUNNING
-</span>
-`;
-
-exports[`StatusTag matches snapshot for unassigned status 1`] = `
-<span
-  className="tag is-warning"
->
-  UNASSIGNED
-</span>
-`;

+ 18 - 21
kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx

@@ -1,26 +1,23 @@
 import React from 'react';
 import { ClusterName } from 'redux/interfaces';
-import { Switch, Route } from 'react-router-dom';
+import { Switch, Route, useParams } from 'react-router-dom';
 import PageLoader from 'components/common/PageLoader/PageLoader';
-import DetailsContainer from 'components/ConsumerGroups/Details/DetailsContainer';
-import ListContainer from 'components/ConsumerGroups/List/ListContainer';
+import Details from 'components/ConsumerGroups/Details/Details';
+import List from 'components/ConsumerGroups/List/List';
+import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import {
+  fetchConsumerGroups,
+  getAreConsumerGroupsFulfilled,
+} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 
-import ResetOffsetsContainer from './Details/ResetOffsets/ResetOffsetsContainer';
-
-interface Props {
-  clusterName: ClusterName;
-  isFetched: boolean;
-  fetchConsumerGroupsList: (clusterName: ClusterName) => void;
-}
-
-const ConsumerGroups: React.FC<Props> = ({
-  clusterName,
-  isFetched,
-  fetchConsumerGroupsList,
-}) => {
+const ConsumerGroups: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const { clusterName } = useParams<{ clusterName: ClusterName }>();
+  const isFetched = useAppSelector(getAreConsumerGroupsFulfilled);
   React.useEffect(() => {
-    fetchConsumerGroupsList(clusterName);
-  }, [fetchConsumerGroupsList, clusterName]);
+    dispatch(fetchConsumerGroups(clusterName));
+  }, [fetchConsumerGroups, clusterName]);
 
   if (isFetched) {
     return (
@@ -28,16 +25,16 @@ const ConsumerGroups: React.FC<Props> = ({
         <Route
           exact
           path="/ui/clusters/:clusterName/consumer-groups"
-          component={ListContainer}
+          component={List}
         />
         <Route
           exact
           path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID"
-          component={DetailsContainer}
+          component={Details}
         />
         <Route
           path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID/reset-offsets"
-          component={ResetOffsetsContainer}
+          component={ResetOffsets}
         />
       </Switch>
     );

+ 0 - 32
kafka-ui-react-app/src/components/ConsumerGroups/ConsumersGroupsContainer.ts

@@ -1,32 +0,0 @@
-import { connect } from 'react-redux';
-import { fetchConsumerGroupsList } from 'redux/actions';
-import { RootState, ClusterName } from 'redux/interfaces';
-import { RouteComponentProps } from 'react-router-dom';
-import { getIsConsumerGroupsListFetched } from 'redux/reducers/consumerGroups/selectors';
-
-import ConsumerGroups from './ConsumerGroups';
-
-interface RouteProps {
-  clusterName: ClusterName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { clusterName },
-    },
-  }: OwnProps
-) => ({
-  isFetched: getIsConsumerGroupsListFetched(state),
-  clusterName,
-});
-
-const mapDispatchToProps = {
-  fetchConsumerGroupsList: (clusterName: ClusterName) =>
-    fetchConsumerGroupsList(clusterName),
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(ConsumerGroups);

+ 101 - 107
kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

@@ -1,56 +1,57 @@
 import React from 'react';
 import { ClusterName } from 'redux/interfaces';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import {
   clusterConsumerGroupResetOffsetsPath,
   clusterConsumerGroupsPath,
 } from 'lib/paths';
 import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
-import {
-  ConsumerGroup,
-  ConsumerGroupDetails,
-  ConsumerGroupTopicPartition,
-} from 'generated-sources';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
-import { useHistory } from 'react-router';
+import { useHistory, useParams } from 'react-router';
 import ClusterContext from 'components/contexts/ClusterContext';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
+import * as Metrics from 'components/common/Metrics';
+import TagStyled from 'components/common/Tag/Tag.styled';
+import Dropdown from 'components/common/Dropdown/Dropdown';
+import DropdownItem from 'components/common/Dropdown/DropdownItem';
+import { Colors } from 'theme/theme';
+import { groupBy } from 'lodash';
+import { Table } from 'components/common/table/Table/Table.styled';
+import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import {
+  fetchConsumerGroupDetails,
+  deleteConsumerGroup,
+  selectById,
+  getIsConsumerGroupDeleted,
+  getAreConsumerGroupDetailsFulfilled,
+} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 
 import ListItem from './ListItem';
 
-export interface Props extends ConsumerGroup, ConsumerGroupDetails {
-  clusterName: ClusterName;
-  partitions?: ConsumerGroupTopicPartition[];
-  isFetched: boolean;
-  isDeleted: boolean;
-  fetchConsumerGroupDetails: (
-    clusterName: ClusterName,
-    consumerGroupID: ConsumerGroupID
-  ) => void;
-  deleteConsumerGroup: (clusterName: string, id: ConsumerGroupID) => void;
-}
-
-const Details: React.FC<Props> = ({
-  clusterName,
-  groupId,
-  partitions,
-  isFetched,
-  isDeleted,
-  fetchConsumerGroupDetails,
-  deleteConsumerGroup,
-}) => {
-  React.useEffect(() => {
-    fetchConsumerGroupDetails(clusterName, groupId);
-  }, [fetchConsumerGroupDetails, clusterName, groupId]);
-  const items = partitions || [];
-  const [isConfirmationModelVisible, setIsConfirmationModelVisible] =
-    React.useState<boolean>(false);
+const Details: React.FC = () => {
   const history = useHistory();
   const { isReadOnly } = React.useContext(ClusterContext);
+  const { consumerGroupID, clusterName } =
+    useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
+  const dispatch = useAppDispatch();
+  const consumerGroup = useAppSelector((state) =>
+    selectById(state, consumerGroupID)
+  );
+  const isDeleted = useAppSelector(getIsConsumerGroupDeleted);
+  const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
+
+  const [isConfirmationModalVisible, setIsConfirmationModalVisible] =
+    React.useState<boolean>(false);
+
+  React.useEffect(() => {
+    dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
+  }, [fetchConsumerGroupDetails, clusterName, consumerGroupID]);
 
   const onDelete = () => {
-    setIsConfirmationModelVisible(false);
-    deleteConsumerGroup(clusterName, groupId);
+    setIsConfirmationModalVisible(false);
+    dispatch(deleteConsumerGroup({ clusterName, consumerGroupID }));
   };
   React.useEffect(() => {
     if (isDeleted) {
@@ -59,83 +60,76 @@ const Details: React.FC<Props> = ({
   }, [isDeleted]);
 
   const onResetOffsets = () => {
-    history.push(clusterConsumerGroupResetOffsetsPath(clusterName, groupId));
+    history.push(
+      clusterConsumerGroupResetOffsetsPath(clusterName, consumerGroupID)
+    );
   };
 
-  return (
-    <div className="section">
-      <div className="level">
-        <div className="level-item level-left">
-          <Breadcrumb
-            links={[
-              {
-                href: clusterConsumerGroupsPath(clusterName),
-                label: 'All Consumer Groups',
-              },
-            ]}
-          >
-            {groupId}
-          </Breadcrumb>
-        </div>
-      </div>
+  if (!isFetched || !consumerGroup) {
+    return <PageLoader />;
+  }
+
+  const partitionsByTopic = groupBy(consumerGroup.partitions, 'topic');
 
-      {isFetched ? (
-        <div className="box">
+  return (
+    <div>
+      <div>
+        <PageHeading text={consumerGroupID}>
           {!isReadOnly && (
-            <div className="level">
-              <div className="level-item level-right buttons">
-                <button
-                  type="button"
-                  className="button"
-                  onClick={onResetOffsets}
-                >
-                  Reset offsets
-                </button>
-                <button
-                  type="button"
-                  className="button is-danger"
-                  onClick={() => setIsConfirmationModelVisible(true)}
-                >
-                  Delete consumer group
-                </button>
-              </div>
-            </div>
+            <Dropdown label={<VerticalElipsisIcon />} right>
+              <DropdownItem onClick={onResetOffsets}>
+                Reset offsest
+              </DropdownItem>
+              <DropdownItem
+                style={{ color: Colors.red[50] }}
+                onClick={() => setIsConfirmationModalVisible(true)}
+              >
+                Delete consumer group
+              </DropdownItem>
+            </Dropdown>
           )}
-
-          <table className="table is-striped is-fullwidth">
-            <thead>
-              <tr>
-                <th>Consumer ID</th>
-                <th>Host</th>
-                <th>Topic</th>
-                <th>Partition</th>
-                <th>Messages behind</th>
-                <th>Current offset</th>
-                <th>End offset</th>
-              </tr>
-            </thead>
-            <tbody>
-              {items.length === 0 && (
-                <tr>
-                  <td colSpan={10}>No active consumer groups</td>
-                </tr>
-              )}
-              {items.map((consumer) => (
-                <ListItem
-                  key={consumer.consumerId}
-                  clusterName={clusterName}
-                  consumer={consumer}
-                />
-              ))}
-            </tbody>
-          </table>
-        </div>
-      ) : (
-        <PageLoader />
-      )}
+        </PageHeading>
+      </div>
+      <Metrics.Wrapper>
+        <Metrics.Section>
+          <Metrics.Indicator label="State">
+            <TagStyled color="yellow">{consumerGroup.state}</TagStyled>
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Members">
+            {consumerGroup.members}
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Assigned topics">
+            {consumerGroup.topics}
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Assigned partitions">
+            {consumerGroup.partitions?.length}
+          </Metrics.Indicator>
+          <Metrics.Indicator label="Coordinator ID">
+            {consumerGroup.coordinator?.id}
+          </Metrics.Indicator>
+        </Metrics.Section>
+      </Metrics.Wrapper>
+      <Table isFullwidth>
+        <thead>
+          <tr>
+            <TableHeaderCell> </TableHeaderCell>
+            <TableHeaderCell title="Topic" />
+          </tr>
+        </thead>
+        <tbody>
+          {Object.keys(partitionsByTopic).map((key) => (
+            <ListItem
+              clusterName={clusterName}
+              consumers={partitionsByTopic[key]}
+              name={key}
+              key={key}
+            />
+          ))}
+        </tbody>
+      </Table>
       <ConfirmationModal
-        isOpen={isConfirmationModelVisible}
-        onCancel={() => setIsConfirmationModelVisible(false)}
+        isOpen={isConfirmationModalVisible}
+        onCancel={() => setIsConfirmationModalVisible(false)}
         onConfirm={onDelete}
       >
         Are you sure you want to delete this consumer group?

+ 0 - 49
kafka-ui-react-app/src/components/ConsumerGroups/Details/DetailsContainer.ts

@@ -1,49 +0,0 @@
-import { connect } from 'react-redux';
-import { ClusterName, RootState } from 'redux/interfaces';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-import {
-  getIsConsumerGroupDetailsFetched,
-  getIsConsumerGroupsDeleted,
-  getConsumerGroupByID,
-} from 'redux/reducers/consumerGroups/selectors';
-import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
-import {
-  deleteConsumerGroup,
-  fetchConsumerGroupDetails,
-} from 'redux/actions/thunks';
-
-import Details from './Details';
-
-interface RouteProps {
-  clusterName: ClusterName;
-  consumerGroupID: ConsumerGroupID;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { consumerGroupID, clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  isFetched: getIsConsumerGroupDetailsFetched(state),
-  isDeleted: getIsConsumerGroupsDeleted(state),
-  ...getConsumerGroupByID(state, consumerGroupID),
-});
-
-const mapDispatchToProps = {
-  fetchConsumerGroupDetails: (
-    clusterName: ClusterName,
-    consumerGroupID: ConsumerGroupID
-  ) => fetchConsumerGroupDetails(clusterName, consumerGroupID),
-  deleteConsumerGroup: (clusterName: string, id: ConsumerGroupID) =>
-    deleteConsumerGroup(clusterName, id),
-};
-
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Details)
-);

+ 6 - 0
kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.styled.ts

@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const ToggleButton = styled.td`
+  padding: 8px 8px 8px 16px !important;
+  width: 30px;
+`;

+ 24 - 21
kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx

@@ -1,34 +1,37 @@
 import React from 'react';
 import { ConsumerGroupTopicPartition } from 'generated-sources';
-import { NavLink } from 'react-router-dom';
+import { Link } from 'react-router-dom';
 import { ClusterName } from 'redux/interfaces/cluster';
 import { clusterTopicPath } from 'lib/paths';
+import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';
+import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
+import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
+
+import TopicContents from './TopicContents/TopicContents';
+import { ToggleButton } from './ListItem.styled';
 
 interface Props {
   clusterName: ClusterName;
-  consumer: ConsumerGroupTopicPartition;
+  name: string;
+  consumers: ConsumerGroupTopicPartition[];
 }
 
-const ListItem: React.FC<Props> = ({ clusterName, consumer }) => {
+const ListItem: React.FC<Props> = ({ clusterName, name, consumers }) => {
+  const [isOpen, setIsOpen] = React.useState(false);
   return (
-    <tr>
-      <td>{consumer.consumerId}</td>
-      <td>{consumer.host}</td>
-      <td>
-        <NavLink
-          exact
-          to={clusterTopicPath(clusterName, consumer.topic)}
-          activeClassName="is-active"
-          className="title is-6"
-        >
-          {consumer.topic}
-        </NavLink>
-      </td>
-      <td>{consumer.partition}</td>
-      <td>{consumer.messagesBehind}</td>
-      <td>{consumer.currentOffset}</td>
-      <td>{consumer.endOffset}</td>
-    </tr>
+    <>
+      <tr>
+        <ToggleButton>
+          <IconButtonWrapper onClick={() => setIsOpen(!isOpen)} aria-hidden>
+            <MessageToggleIcon isOpen={isOpen} />
+          </IconButtonWrapper>
+        </ToggleButton>
+        <TableKeyLink>
+          <Link to={clusterTopicPath(clusterName, name)}>{name}</Link>
+        </TableKeyLink>
+      </tr>
+      {isOpen && <TopicContents consumers={consumers} />}
+    </>
   );
 };
 

+ 69 - 0
kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.styled.ts

@@ -0,0 +1,69 @@
+import styled from 'styled-components';
+
+export const ResetOffsetsStyledWrapper = styled.div`
+  padding: 16px;
+  padding-top: 0;
+
+  & > form {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+
+    & > button:last-child {
+      align-self: flex-start;
+    }
+  }
+
+  & .multi-select {
+    height: 32px;
+    & > .dropdown-container {
+      height: 32px;
+      & > .dropdown-heading {
+        height: 32px;
+      }
+    }
+  }
+
+  & .date-picker {
+    height: 32px;
+    border: 1px ${(props) => props.theme.selectStyles.borderColor.normal} solid;
+    border-radius: 4px;
+    font-size: 14px;
+    width: 50%;
+    padding-left: 12px;
+    color: ${(props) => props.theme.selectStyles.color.normal};
+
+    background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
+    background-repeat: no-repeat !important;
+    background-position-x: 96% !important;
+    background-position-y: 55% !important;
+    appearance: none !important;
+
+    &:hover {
+      cursor: pointer;
+    }
+    &:focus {
+      outline: none;
+    }
+  }
+`;
+
+export const MainSelectorsWrapperStyled = styled.div`
+  display: flex;
+  gap: 16px;
+  & > * {
+    flex-grow: 1;
+  }
+`;
+
+export const OffsetsWrapperStyled = styled.div`
+  display: flex;
+  width: 100%;
+  flex-wrap: wrap;
+  gap: 16px;
+`;
+
+export const OffsetsTitleStyled = styled.h1`
+  font-size: 18px;
+  font-weight: 500;
+`;

+ 141 - 178
kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx

@@ -1,14 +1,12 @@
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
-import {
-  ConsumerGroupDetails,
-  ConsumerGroupOffsetsResetType,
-} from 'generated-sources';
-import {
-  clusterConsumerGroupsPath,
-  clusterConsumerGroupDetailsPath,
-} from 'lib/paths';
+import { ConsumerGroupOffsetsResetType } from 'generated-sources';
+import { clusterConsumerGroupDetailsPath } from 'lib/paths';
 import React from 'react';
-import { Controller, useFieldArray, useForm } from 'react-hook-form';
+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';
@@ -17,31 +15,29 @@ 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 } from 'react-router';
+import { useHistory, useParams } from 'react-router';
+import Select from 'components/common/Select/Select';
+import { InputLabel } from 'components/common/Input/InputLabel.styled';
+import { Button } from 'components/common/Button/Button';
+import Input from 'components/common/Input/Input';
+import { FormError } from 'components/common/Input/Input.styled';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import {
+  fetchConsumerGroupDetails,
+  selectById,
+  getAreConsumerGroupDetailsFulfilled,
+  getIsOffsetReseted,
+  resetConsumerGroupOffsets,
+} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
 
-export interface Props {
-  clusterName: ClusterName;
-  consumerGroupID: ConsumerGroupID;
-  consumerGroup: ConsumerGroupDetails;
-  detailsAreFetched: boolean;
-  IsOffsetReset: boolean;
-  fetchConsumerGroupDetails(
-    clusterName: ClusterName,
-    consumerGroupID: ConsumerGroupID
-  ): void;
-  resetConsumerGroupOffsets(
-    clusterName: ClusterName,
-    consumerGroupID: ConsumerGroupID,
-    requestBody: {
-      topic: string;
-      resetType: ConsumerGroupOffsetsResetType;
-      partitionsOffsets?: { offset: string; partition: number }[];
-      resetToTimestamp?: Date;
-      partitions: number[];
-    }
-  ): void;
-  resetResettingStatus: () => void;
-}
+import {
+  MainSelectorsWrapperStyled,
+  OffsetsWrapperStyled,
+  ResetOffsetsStyledWrapper,
+  OffsetsTitleStyled,
+} from './ResetOffsets.styled';
 
 interface FormType {
   topic: string;
@@ -50,18 +46,19 @@ interface FormType {
   resetToTimestamp: Date;
 }
 
-const ResetOffsets: React.FC<Props> = ({
-  clusterName,
-  consumerGroupID,
-  consumerGroup,
-  detailsAreFetched,
-  IsOffsetReset,
-  fetchConsumerGroupDetails,
-  resetConsumerGroupOffsets,
-  resetResettingStatus,
-}) => {
+const ResetOffsets: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const { consumerGroupID, clusterName } =
+    useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
+  const consumerGroup = useAppSelector((state) =>
+    selectById(state, consumerGroupID)
+  );
+
+  const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
+  const isOffsetReseted = useAppSelector(getIsOffsetReseted);
+
   React.useEffect(() => {
-    fetchConsumerGroupDetails(clusterName, consumerGroupID);
+    dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
   }, [clusterName, consumerGroupID]);
 
   const [uniqueTopics, setUniqueTopics] = React.useState<string[]>([]);
@@ -69,8 +66,14 @@ const ResetOffsets: React.FC<Props> = ({
     []
   );
 
+  const methods = useForm<FormType>({
+    defaultValues: {
+      resetType: ConsumerGroupOffsetsResetType.EARLIEST,
+      topic: '',
+      partitionsOffsets: [],
+    },
+  });
   const {
-    register,
     handleSubmit,
     setValue,
     watch,
@@ -78,13 +81,7 @@ const ResetOffsets: React.FC<Props> = ({
     setError,
     clearErrors,
     formState: { errors },
-  } = useForm<FormType>({
-    defaultValues: {
-      resetType: ConsumerGroupOffsetsResetType.EARLIEST,
-      topic: '',
-      partitionsOffsets: [],
-    },
-  });
+  } = methods;
   const { fields } = useFieldArray({
     control,
     name: 'partitionsOffsets',
@@ -94,11 +91,11 @@ const ResetOffsets: React.FC<Props> = ({
   const offsetsValue = watch('partitionsOffsets');
 
   React.useEffect(() => {
-    if (detailsAreFetched && consumerGroup.partitions) {
+    if (isFetched && consumerGroup?.partitions) {
       setValue('topic', consumerGroup.partitions[0].topic);
       setUniqueTopics(Object.keys(groupBy(consumerGroup.partitions, 'topic')));
     }
-  }, [detailsAreFetched]);
+  }, [isFetched]);
 
   const onSelectedPartitionsChange = (value: Option[]) => {
     clearErrors();
@@ -153,174 +150,140 @@ const ResetOffsets: React.FC<Props> = ({
       }
     }
     if (isValid) {
-      resetConsumerGroupOffsets(clusterName, consumerGroupID, augmentedData);
+      dispatch(
+        resetConsumerGroupOffsets({
+          clusterName,
+          consumerGroupID,
+          requestBody: augmentedData,
+        })
+      );
     }
   };
 
   const history = useHistory();
   React.useEffect(() => {
-    if (IsOffsetReset) {
-      resetResettingStatus();
+    if (isOffsetReseted) {
+      dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets'));
       history.push(
         clusterConsumerGroupDetailsPath(clusterName, consumerGroupID)
       );
     }
-  }, [IsOffsetReset]);
+  }, [isOffsetReseted]);
 
-  if (!detailsAreFetched) {
+  if (!isFetched || !consumerGroup) {
     return <PageLoader />;
   }
 
   return (
-    <div className="section">
-      <div className="level">
-        <div className="level-item level-left">
-          <Breadcrumb
-            links={[
-              {
-                href: clusterConsumerGroupsPath(clusterName),
-                label: 'All Consumer Groups',
-              },
-              {
-                href: clusterConsumerGroupDetailsPath(
-                  clusterName,
-                  consumerGroupID
-                ),
-                label: consumerGroupID,
-              },
-            ]}
-          >
-            Reset Offsets
-          </Breadcrumb>
-        </div>
-      </div>
-
-      <div className="box">
+    <FormProvider {...methods}>
+      <PageHeading text="Reset offsets" />
+      <ResetOffsetsStyledWrapper>
         <form onSubmit={handleSubmit(onSubmit)}>
-          <div className="columns">
-            <div className="column is-one-third">
-              <label className="label" htmlFor="topic">
-                Topic
-              </label>
-              <div className="select">
-                <select {...register('topic')} id="topic">
-                  {uniqueTopics.map((topic) => (
-                    <option key={topic} value={topic}>
-                      {topic}
-                    </option>
-                  ))}
-                </select>
-              </div>
+          <MainSelectorsWrapperStyled>
+            <div>
+              <InputLabel htmlFor="topic">Topic</InputLabel>
+              <Select name="topic" id="topic" selectSize="M">
+                {uniqueTopics.map((topic) => (
+                  <option key={topic} value={topic}>
+                    {topic}
+                  </option>
+                ))}
+              </Select>
             </div>
-            <div className="column is-one-third">
-              <label className="label" htmlFor="resetType">
-                Reset Type
-              </label>
-              <div className="select">
-                <select {...register('resetType')} id="resetType">
-                  {Object.values(ConsumerGroupOffsetsResetType).map((type) => (
-                    <option key={type} value={type}>
-                      {type}
-                    </option>
-                  ))}
-                </select>
-              </div>
+            <div>
+              <InputLabel htmlFor="resetType">Reset Type</InputLabel>
+              <Select name="resetType" id="resetType" selectSize="M">
+                {Object.values(ConsumerGroupOffsetsResetType).map((type) => (
+                  <option key={type} value={type}>
+                    {type}
+                  </option>
+                ))}
+              </Select>
             </div>
-            <div className="column is-one-third">
-              <label className="label">Partitions</label>
-              <div className="select">
-                <MultiSelect
-                  options={
-                    consumerGroup.partitions
-                      ?.filter((p) => p.topic === topicValue)
-                      .map((p) => ({
-                        label: `Partition #${p.partition.toString()}`,
-                        value: p.partition,
-                      })) || []
-                  }
-                  value={selectedPartitions}
-                  onChange={onSelectedPartitionsChange}
-                  labelledBy="Select partitions"
-                />
-              </div>
+            <div>
+              <InputLabel>Partitions</InputLabel>
+              <MultiSelect
+                options={
+                  consumerGroup.partitions
+                    ?.filter((p) => p.topic === topicValue)
+                    .map((p) => ({
+                      label: `Partition #${p.partition.toString()}`,
+                      value: p.partition,
+                    })) || []
+                }
+                value={selectedPartitions}
+                onChange={onSelectedPartitionsChange}
+                labelledBy="Select partitions"
+              />
             </div>
-          </div>
+          </MainSelectorsWrapperStyled>
           {resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP &&
             selectedPartitions.length > 0 && (
-              <div className="columns">
-                <div className="column is-half">
-                  <label className="label">Timestamp</label>
-                  <Controller
-                    control={control}
-                    name="resetToTimestamp"
-                    render={({ field: { onChange, onBlur, value, ref } }) => (
-                      <DatePicker
-                        ref={ref}
-                        selected={value}
-                        onChange={onChange}
-                        onBlur={onBlur}
-                        showTimeInput
-                        timeInputLabel="Time:"
-                        dateFormat="MMMM d, yyyy h:mm aa"
-                        className="input"
-                      />
-                    )}
-                  />
-                  <ErrorMessage
-                    errors={errors}
-                    name="resetToTimestamp"
-                    render={({ message }) => (
-                      <p className="help is-danger">{message}</p>
-                    )}
-                  />
-                </div>
+              <div>
+                <InputLabel>Timestamp</InputLabel>
+                <Controller
+                  control={control}
+                  name="resetToTimestamp"
+                  render={({ field: { onChange, onBlur, value, ref } }) => (
+                    <DatePicker
+                      ref={ref}
+                      selected={value}
+                      onChange={onChange}
+                      onBlur={onBlur}
+                      showTimeInput
+                      timeInputLabel="Time:"
+                      dateFormat="MMMM d, yyyy h:mm aa"
+                      className="date-picker"
+                    />
+                  )}
+                />
+                <ErrorMessage
+                  errors={errors}
+                  name="resetToTimestamp"
+                  render={({ message }) => <FormError>{message}</FormError>}
+                />
               </div>
             )}
           {resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET &&
             selectedPartitions.length > 0 && (
-              <div className="columns">
-                <div className="column is-one-third">
-                  <label className="label">Offsets</label>
+              <div>
+                <OffsetsTitleStyled>Offsets</OffsetsTitleStyled>
+                <OffsetsWrapperStyled>
                   {fields.map((field, index) => (
-                    <div key={field.id} className="mb-2">
-                      <label
-                        className="subtitle is-6"
-                        htmlFor={`partitionsOffsets.${index}.offset`}
-                      >
+                    <div key={field.id}>
+                      <InputLabel htmlFor={`partitionsOffsets.${index}.offset`}>
                         Partition #{field.partition}
-                      </label>
-                      <input
+                      </InputLabel>
+                      <Input
                         id={`partitionsOffsets.${index}.offset`}
                         type="number"
-                        className="input"
-                        {...register(
-                          `partitionsOffsets.${index}.offset` as const,
-                          { shouldUnregister: true }
-                        )}
+                        name={`partitionsOffsets.${index}.offset` as const}
+                        hookFormOptions={{ shouldUnregister: true }}
                         defaultValue={field.offset}
                       />
                       <ErrorMessage
                         errors={errors}
                         name={`partitionsOffsets.${index}.offset`}
                         render={({ message }) => (
-                          <p className="help is-danger">{message}</p>
+                          <FormError>{message}</FormError>
                         )}
                       />
                     </div>
                   ))}
-                </div>
+                </OffsetsWrapperStyled>
               </div>
             )}
-          <button
-            className="button is-primary"
+          <Button
+            buttonSize="M"
+            buttonType="primary"
             type="submit"
             disabled={selectedPartitions.length === 0}
           >
             Submit
-          </button>
+          </Button>
         </form>
-      </div>
-    </div>
+      </ResetOffsetsStyledWrapper>
+    </FormProvider>
   );
 };
 

+ 0 - 47
kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsetsContainer.ts

@@ -1,47 +0,0 @@
-import { RouteComponentProps, withRouter } from 'react-router-dom';
-import { connect } from 'react-redux';
-import { ClusterName, ConsumerGroupID, RootState } from 'redux/interfaces';
-import {
-  getConsumerGroupByID,
-  getIsConsumerGroupDetailsFetched,
-  getOffsetReset,
-} from 'redux/reducers/consumerGroups/selectors';
-import {
-  fetchConsumerGroupDetails,
-  resetConsumerGroupOffsets,
-  resetConsumerGroupOffsetsAction,
-} from 'redux/actions';
-
-import ResetOffsets from './ResetOffsets';
-
-interface RouteProps {
-  clusterName: ClusterName;
-  consumerGroupID: ConsumerGroupID;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { consumerGroupID, clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  consumerGroupID,
-  consumerGroup: getConsumerGroupByID(state, consumerGroupID),
-  detailsAreFetched: getIsConsumerGroupDetailsFetched(state),
-  IsOffsetReset: getOffsetReset(state),
-});
-
-const mapDispatchToProps = {
-  fetchConsumerGroupDetails,
-  resetConsumerGroupOffsets,
-  resetResettingStatus: resetConsumerGroupOffsetsAction.cancel,
-};
-
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(ResetOffsets)
-);

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

@@ -1,178 +1,162 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react';
-import ResetOffsets, {
-  Props,
-} from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
-import { ConsumerGroupState } from 'generated-sources';
 import React from 'react';
-import { StaticRouter } from 'react-router';
+import fetchMock from 'fetch-mock';
+import { Route, StaticRouter } from 'react-router';
+import { screen, waitFor, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from 'lib/testHelpers';
+import { clusterConsumerGroupResetOffsetsPath } from 'lib/paths';
+import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
+import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
 
-import { expectedOutputs } from './fixtures';
+const clusterName = 'cluster1';
+const { groupId } = consumerGroupPayload;
 
-const setupWrapper = (props?: Partial<Props>) => (
-  <StaticRouter>
-    <ResetOffsets
-      clusterName="testCluster"
-      consumerGroupID="testGroup"
-      consumerGroup={{
-        groupId: 'amazon.msk.canary.group.broker-1',
-        members: 0,
-        topics: 2,
-        simple: false,
-        partitionAssignor: '',
-        state: ConsumerGroupState.EMPTY,
-        coordinator: {
-          id: 2,
-          host: 'b-2.kad-msk.st2jzq.c6.kafka.eu-west-1.amazonaws.com',
-        },
-        messagesBehind: 0,
-        partitions: [
-          {
-            topic: '__amazon_msk_canary',
-            partition: 1,
-            currentOffset: 0,
-            endOffset: 0,
-            messagesBehind: 0,
-            consumerId: undefined,
-            host: undefined,
-          },
-          {
-            topic: '__amazon_msk_canary',
-            partition: 0,
-            currentOffset: 56932,
-            endOffset: 56932,
-            messagesBehind: 0,
-            consumerId: undefined,
-            host: undefined,
-          },
-          {
-            topic: 'other_topic',
-            partition: 3,
-            currentOffset: 56932,
-            endOffset: 56932,
-            messagesBehind: 0,
-            consumerId: undefined,
-            host: undefined,
-          },
-          {
-            topic: 'other_topic',
-            partition: 4,
-            currentOffset: 56932,
-            endOffset: 56932,
-            messagesBehind: 0,
-            consumerId: undefined,
-            host: undefined,
-          },
-        ],
+const renderComponent = () =>
+  render(
+    <StaticRouter
+      location={{
+        pathname: clusterConsumerGroupResetOffsetsPath(
+          clusterName,
+          consumerGroupPayload.groupId
+        ),
       }}
-      detailsAreFetched
-      IsOffsetReset={false}
-      fetchConsumerGroupDetails={jest.fn()}
-      resetConsumerGroupOffsets={jest.fn()}
-      resetResettingStatus={jest.fn()}
-      {...props}
-    />
-  </StaticRouter>
-);
+    >
+      <Route
+        path={clusterConsumerGroupResetOffsetsPath(
+          ':clusterName',
+          ':consumerGroupID'
+        )}
+      >
+        <ResetOffsets />
+      </Route>
+    </StaticRouter>
+  );
+
+const resetConsumerGroupOffsetsMockCalled = () =>
+  expect(
+    fetchMock.called(
+      `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`
+    )
+  ).toBeTruthy();
 
 const selectresetTypeAndPartitions = async (resetType: string) => {
-  fireEvent.change(screen.getByLabelText('Reset Type'), {
-    target: { value: resetType },
-  });
+  userEvent.selectOptions(screen.getByLabelText('Reset Type'), resetType);
+  userEvent.click(screen.getByText('Select...'));
   await waitFor(() => {
-    fireEvent.click(screen.getByText('Select...'));
+    userEvent.click(screen.getByText('Partition #0'));
   });
+};
+
+const resetConsumerGroupOffsetsWith = async (
+  resetType: string,
+  offset: null | number = null
+) => {
+  userEvent.selectOptions(screen.getByLabelText('Reset Type'), resetType);
+  userEvent.click(screen.getByText('Select...'));
   await waitFor(() => {
-    fireEvent.click(screen.getByText('Partition #0'));
+    userEvent.click(screen.getByText('Partition #0'));
   });
+  fetchMock.postOnce(
+    `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`,
+    200,
+    {
+      body: {
+        topic: '__amazon_msk_canary',
+        resetType,
+        partitions: [0],
+        partitionsOffsets: [{ partition: 0, offset }],
+      },
+    }
+  );
+  userEvent.click(screen.getByText('Submit'));
+  await waitFor(() => resetConsumerGroupOffsetsMockCalled());
 };
 
 describe('ResetOffsets', () => {
-  describe('on initial render', () => {
-    const component = render(setupWrapper());
-    it('matches the snapshot', () => {
-      expect(component.baseElement).toMatchSnapshot();
-    });
+  afterEach(() => {
+    fetchMock.reset();
   });
 
-  describe('on submit', () => {
-    describe('with the default ResetType', () => {
-      it('calls resetConsumerGroupOffsets', async () => {
-        const mockResetConsumerGroupOffsets = jest.fn();
-        render(
-          setupWrapper({
-            resetConsumerGroupOffsets: mockResetConsumerGroupOffsets,
-          })
+  it('renders progress bar for initial state', () => {
+    fetchMock.getOnce(
+      `/api/clusters/${clusterName}/consumer-groups/${groupId}`,
+      404
+    );
+    renderComponent();
+    expect(screen.getByRole('progressbar')).toBeInTheDocument();
+  });
+
+  describe('with consumer group', () => {
+    describe('submit handles resetConsumerGroupOffsets', () => {
+      beforeEach(async () => {
+        const fetchConsumerGroupMock = fetchMock.getOnce(
+          `/api/clusters/${clusterName}/consumer-groups/${groupId}`,
+          consumerGroupPayload
         );
-        await selectresetTypeAndPartitions('EARLIEST');
-        await waitFor(() => {
-          fireEvent.click(screen.getByText('Submit'));
-        });
-        expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(1);
-        expect(mockResetConsumerGroupOffsets).toHaveBeenCalledWith(
-          'testCluster',
-          'testGroup',
-          expectedOutputs.EARLIEST
+        renderComponent();
+        await waitFor(() =>
+          expect(fetchConsumerGroupMock.called()).toBeTruthy()
         );
+        await waitFor(() => screen.queryByRole('form'));
       });
-    });
 
-    describe('with the ResetType set to LATEST', () => {
-      it('calls resetConsumerGroupOffsets', async () => {
-        const mockResetConsumerGroupOffsets = jest.fn();
-        render(
-          setupWrapper({
-            resetConsumerGroupOffsets: mockResetConsumerGroupOffsets,
-          })
-        );
-        await selectresetTypeAndPartitions('LATEST');
-        await waitFor(() => {
-          fireEvent.click(screen.getByText('Submit'));
-        });
-        expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(1);
-        expect(mockResetConsumerGroupOffsets).toHaveBeenCalledWith(
-          'testCluster',
-          'testGroup',
-          expectedOutputs.LATEST
-        );
+      it('calls resetConsumerGroupOffsets with EARLIEST', async () => {
+        await resetConsumerGroupOffsetsWith('EARLIEST');
       });
-    });
 
-    describe('with the ResetType set to OFFSET', () => {
-      it('calls resetConsumerGroupOffsets', async () => {
-        const mockResetConsumerGroupOffsets = jest.fn();
-        render(
-          setupWrapper({
-            resetConsumerGroupOffsets: mockResetConsumerGroupOffsets,
-          })
-        );
+      it('calls resetConsumerGroupOffsets with LATEST', async () => {
+        await resetConsumerGroupOffsetsWith('LATEST');
+      });
+      it('calls resetConsumerGroupOffsets with OFFSET', async () => {
         await selectresetTypeAndPartitions('OFFSET');
+        fetchMock.postOnce(
+          `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`,
+          200,
+          {
+            body: {
+              topic: '__amazon_msk_canary',
+              resetType: 'OFFSET',
+              partitions: [0],
+              partitionsOffsets: [{ partition: 0, offset: 10 }],
+            },
+          }
+        );
         await waitFor(() => {
           fireEvent.change(screen.getAllByLabelText('Partition #0')[1], {
             target: { value: '10' },
           });
         });
-        await waitFor(() => {
-          fireEvent.click(screen.getByText('Submit'));
-        });
-        expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(1);
-        expect(mockResetConsumerGroupOffsets).toHaveBeenCalledWith(
-          'testCluster',
-          'testGroup',
-          expectedOutputs.OFFSET
-        );
+        userEvent.click(screen.getByText('Submit'));
+        await waitFor(() => resetConsumerGroupOffsetsMockCalled());
       });
-    });
-
-    describe('with the ResetType set to TIMESTAMP', () => {
-      it('adds error to the page when the input is left empty', async () => {
-        const mockResetConsumerGroupOffsets = jest.fn();
-        render(setupWrapper());
+      it('calls resetConsumerGroupOffsets with TIMESTAMP', async () => {
         await selectresetTypeAndPartitions('TIMESTAMP');
-        await waitFor(() => {
-          fireEvent.click(screen.getByText('Submit'));
-        });
-        expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(0);
-        expect(screen.getByText("This field shouldn't be empty!")).toBeTruthy();
+        const resetConsumerGroupOffsetsMock = fetchMock.postOnce(
+          `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`,
+          200,
+          {
+            body: {
+              topic: '__amazon_msk_canary',
+              resetType: 'OFFSET',
+              partitions: [0],
+              partitionsOffsets: [{ partition: 0, offset: 10 }],
+            },
+          }
+        );
+        userEvent.click(screen.getByText('Submit'));
+        await waitFor(() =>
+          expect(
+            screen.getByText("This field shouldn't be empty!")
+          ).toBeInTheDocument()
+        );
+
+        await waitFor(() =>
+          expect(
+            resetConsumerGroupOffsetsMock.called(
+              `/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`
+            )
+          ).toBeFalsy()
+        );
       });
     });
   });

+ 0 - 184
kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/__snapshots__/ResetOffsets.spec.tsx.snap

@@ -1,184 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ResetOffsets on initial render matches the snapshot 1`] = `
-<body>
-  <div>
-    <div
-      class="section"
-    >
-      <div
-        class="level"
-      >
-        <div
-          class="level-item level-left"
-        >
-          <nav
-            aria-label="breadcrumbs"
-            class="breadcrumb"
-          >
-            <ul>
-              <li>
-                <a
-                  href="/ui/clusters/testCluster/consumer-groups"
-                >
-                  All Consumer Groups
-                </a>
-              </li>
-              <li>
-                <a
-                  href="/ui/clusters/testCluster/consumer-groups/testGroup"
-                >
-                  testGroup
-                </a>
-              </li>
-              <li
-                class="is-active"
-              >
-                <span
-                  class=""
-                >
-                  Reset Offsets
-                </span>
-              </li>
-            </ul>
-          </nav>
-        </div>
-      </div>
-      <div
-        class="box"
-      >
-        <form>
-          <div
-            class="columns"
-          >
-            <div
-              class="column is-one-third"
-            >
-              <label
-                class="label"
-                for="topic"
-              >
-                Topic
-              </label>
-              <div
-                class="select"
-              >
-                <select
-                  id="topic"
-                  name="topic"
-                >
-                  <option
-                    value="__amazon_msk_canary"
-                  >
-                    __amazon_msk_canary
-                  </option>
-                  <option
-                    value="other_topic"
-                  >
-                    other_topic
-                  </option>
-                </select>
-              </div>
-            </div>
-            <div
-              class="column is-one-third"
-            >
-              <label
-                class="label"
-                for="resetType"
-              >
-                Reset Type
-              </label>
-              <div
-                class="select"
-              >
-                <select
-                  id="resetType"
-                  name="resetType"
-                >
-                  <option
-                    value="EARLIEST"
-                  >
-                    EARLIEST
-                  </option>
-                  <option
-                    value="LATEST"
-                  >
-                    LATEST
-                  </option>
-                  <option
-                    value="TIMESTAMP"
-                  >
-                    TIMESTAMP
-                  </option>
-                  <option
-                    value="OFFSET"
-                  >
-                    OFFSET
-                  </option>
-                </select>
-              </div>
-            </div>
-            <div
-              class="column is-one-third"
-            >
-              <label
-                class="label"
-              >
-                Partitions
-              </label>
-              <div
-                class="select"
-              >
-                <div
-                  class="rmsc multi-select"
-                >
-                  <div
-                    aria-labelledby="Select partitions"
-                    aria-readonly="true"
-                    class="dropdown-container"
-                    tabindex="0"
-                  >
-                    <div
-                      class="dropdown-heading"
-                    >
-                      <div
-                        class="dropdown-heading-value"
-                      >
-                        <span
-                          class="gray"
-                        >
-                          Select...
-                        </span>
-                      </div>
-                      <svg
-                        class="dropdown-heading-dropdown-arrow gray"
-                        fill="none"
-                        height="24"
-                        stroke="currentColor"
-                        stroke-width="2"
-                        width="24"
-                      >
-                        <path
-                          d="M6 9L12 15 18 9"
-                        />
-                      </svg>
-                    </div>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-          <button
-            class="button is-primary"
-            disabled=""
-            type="submit"
-          >
-            Submit
-          </button>
-        </form>
-      </div>
-    </div>
-  </div>
-</body>
-`;

+ 15 - 0
kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContent.styled.ts

@@ -0,0 +1,15 @@
+import styled from 'styled-components';
+import { Colors } from 'theme/theme';
+
+export const TopicContentWrapper = styled.tr`
+  background-color: ${Colors.neutral[5]};
+  & > td {
+    padding: 16px !important;
+  }
+`;
+
+export const ContentBox = styled.div`
+  background-color: white;
+  padding: 20px;
+  border-radius: 8px;
+`;

+ 47 - 0
kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx

@@ -0,0 +1,47 @@
+import { Table } from 'components/common/table/Table/Table.styled';
+import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
+import { ConsumerGroupTopicPartition } from 'generated-sources';
+import React from 'react';
+
+import { ContentBox, TopicContentWrapper } from './TopicContent.styled';
+
+interface Props {
+  consumers: ConsumerGroupTopicPartition[];
+}
+
+const TopicContents: React.FC<Props> = ({ consumers }) => {
+  return (
+    <TopicContentWrapper>
+      <td colSpan={3}>
+        <ContentBox>
+          <Table isFullwidth>
+            <thead>
+              <tr>
+                <TableHeaderCell title="Partition" />
+                <TableHeaderCell title="Consumer ID" />
+                <TableHeaderCell title="Host" />
+                <TableHeaderCell title="Messages behind" />
+                <TableHeaderCell title="Current offset" />
+                <TableHeaderCell title="End offset" />
+              </tr>
+            </thead>
+            <tbody>
+              {consumers.map((consumer) => (
+                <tr key={consumer.partition}>
+                  <td>{consumer.partition}</td>
+                  <td>{consumer.consumerId}</td>
+                  <td>{consumer.host}</td>
+                  <td>{consumer.messagesBehind}</td>
+                  <td>{consumer.currentOffset}</td>
+                  <td>{consumer.endOffset}</td>
+                </tr>
+              ))}
+            </tbody>
+          </Table>
+        </ContentBox>
+      </td>
+    </TopicContentWrapper>
+  );
+};
+
+export default TopicContents;

+ 99 - 86
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx

@@ -1,102 +1,115 @@
-import Details, { Props } from 'components/ConsumerGroups/Details/Details';
-import { mount, shallow } from 'enzyme';
+import Details from 'components/ConsumerGroups/Details/Details';
 import React from 'react';
-import { StaticRouter } from 'react-router';
+import fetchMock from 'fetch-mock';
+import { createMemoryHistory } from 'history';
+import { render } from 'lib/testHelpers';
+import { Route, Router } from 'react-router';
+import {
+  clusterConsumerGroupDetailsPath,
+  clusterConsumerGroupResetOffsetsPath,
+  clusterConsumerGroupsPath,
+} from 'lib/paths';
+import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
+import {
+  screen,
+  waitFor,
+  waitForElementToBeRemoved,
+} from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
 
-const mockHistory = {
-  push: jest.fn(),
-};
-jest.mock('react-router', () => ({
-  ...jest.requireActual('react-router'),
-  useHistory: () => mockHistory,
-}));
+const clusterName = 'cluster1';
+const { groupId } = consumerGroupPayload;
+const history = createMemoryHistory();
 
-describe('Details component', () => {
-  const setupWrapper = (props?: Partial<Props>) => (
-    <Details
-      clusterName="local"
-      groupId="test"
-      isFetched
-      isDeleted={false}
-      fetchConsumerGroupDetails={jest.fn()}
-      deleteConsumerGroup={jest.fn()}
-      partitions={[
-        {
-          consumerId:
-            'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0',
-          topic: 'messages',
-          host: '/172.31.9.153',
-          partition: 6,
-          currentOffset: 394,
-          endOffset: 394,
-          messagesBehind: 0,
-        },
-        {
-          consumerId:
-            'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1',
-          topic: 'messages',
-          host: '/172.31.9.153',
-          partition: 7,
-          currentOffset: 384,
-          endOffset: 384,
-          messagesBehind: 0,
-        },
-      ]}
-      {...props}
-    />
+const renderComponent = () => {
+  history.push(clusterConsumerGroupDetailsPath(clusterName, groupId));
+  render(
+    <Router history={history}>
+      <Route
+        path={clusterConsumerGroupDetailsPath(
+          ':clusterName',
+          ':consumerGroupID'
+        )}
+      >
+        <Details />
+      </Route>
+    </Router>
   );
+};
+describe('Details component', () => {
+  afterEach(() => {
+    fetchMock.reset();
+  });
+
   describe('when consumer gruops are NOT fetched', () => {
-    it('Matches the snapshot', () => {
-      expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
+    it('renders progress bar for initial state', () => {
+      fetchMock.getOnce(
+        `/api/clusters/${clusterName}/consumer-groups/${groupId}`,
+        404
+      );
+      renderComponent();
+      expect(screen.getByRole('progressbar')).toBeInTheDocument();
     });
   });
 
   describe('when consumer gruops are fetched', () => {
-    it('Matches the snapshot', () => {
-      expect(shallow(setupWrapper())).toMatchSnapshot();
+    beforeEach(async () => {
+      const fetchConsumerGroupMock = fetchMock.getOnce(
+        `/api/clusters/${clusterName}/consumer-groups/${groupId}`,
+        consumerGroupPayload
+      );
+      renderComponent();
+      await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
+      await waitFor(() => expect(fetchConsumerGroupMock.called()).toBeTruthy());
+    });
+
+    it('renders component', () => {
+      expect(screen.getByRole('heading')).toBeInTheDocument();
+      expect(screen.getByText(groupId)).toBeInTheDocument();
+
+      expect(screen.getByRole('table')).toBeInTheDocument();
+      expect(screen.getAllByRole('columnheader').length).toEqual(2);
+
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+    });
+
+    it('hanles [Reset offsest] click', async () => {
+      userEvent.click(screen.getByText('Reset offsest'));
+      expect(history.location.pathname).toEqual(
+        clusterConsumerGroupResetOffsetsPath(clusterName, groupId)
+      );
+    });
+
+    it('shows confirmation modal on consumer group delete', async () => {
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+      userEvent.click(screen.getByText('Delete consumer group'));
+      await waitFor(() =>
+        expect(screen.queryByRole('dialog')).toBeInTheDocument()
+      );
+      userEvent.click(screen.getByText('Cancel'));
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
     });
 
-    describe('onDelete', () => {
-      it('calls deleteConsumerGroup', () => {
-        const deleteConsumerGroup = jest.fn();
-        const component = mount(
-          <StaticRouter>{setupWrapper({ deleteConsumerGroup })}</StaticRouter>
-        );
-        component.find('button').at(1).simulate('click');
-        component.update();
-        component
-          .find('ConfirmationModal')
-          .find('button')
-          .at(1)
-          .simulate('click');
-        expect(deleteConsumerGroup).toHaveBeenCalledTimes(1);
-      });
+    it('hanles [Delete consumer group] click', async () => {
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+      userEvent.click(screen.getByText('Delete consumer group'));
 
-      describe('on ConfirmationModal cancel', () => {
-        it('does not call deleteConsumerGroup', () => {
-          const deleteConsumerGroup = jest.fn();
-          const component = mount(
-            <StaticRouter>{setupWrapper({ deleteConsumerGroup })}</StaticRouter>
-          );
-          component.find('button').at(1).simulate('click');
-          component.update();
-          component
-            .find('ConfirmationModal')
-            .find('button')
-            .at(0)
-            .simulate('click');
-          expect(deleteConsumerGroup).toHaveBeenCalledTimes(0);
-        });
-      });
+      await waitFor(() =>
+        expect(screen.queryByRole('dialog')).toBeInTheDocument()
+      );
 
-      describe('after deletion', () => {
-        it('calls history.push', () => {
-          mount(
-            <StaticRouter>{setupWrapper({ isDeleted: true })}</StaticRouter>
-          );
-          expect(mockHistory.push).toHaveBeenCalledTimes(1);
-        });
-      });
+      const deleteConsumerGroupMock = fetchMock.deleteOnce(
+        `/api/clusters/${clusterName}/consumer-groups/${groupId}`,
+        200
+      );
+      userEvent.click(screen.getByText('Submit'));
+      await waitFor(() =>
+        expect(deleteConsumerGroupMock.called()).toBeTruthy()
+      );
+      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+      expect(history.location.pathname).toEqual(
+        clusterConsumerGroupsPath(clusterName)
+      );
     });
   });
 });

+ 0 - 8
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/DetailsContainer.spec.tsx

@@ -1,8 +0,0 @@
-import React from 'react';
-import { containerRendersView } from 'lib/testHelpers';
-import Details from 'components/ConsumerGroups/Details/Details';
-import DetailsContainer from 'components/ConsumerGroups/Details/DetailsContainer';
-
-describe('DetailsContainer', () => {
-  containerRendersView(<DetailsContainer />, Details);
-});

+ 0 - 157
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/__snapshots__/Details.spec.tsx.snap

@@ -1,157 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Details component when consumer gruops are NOT fetched Matches the snapshot 1`] = `
-<div
-  className="section"
->
-  <div
-    className="level"
-  >
-    <div
-      className="level-item level-left"
-    >
-      <Breadcrumb
-        links={
-          Array [
-            Object {
-              "href": "/ui/clusters/local/consumer-groups",
-              "label": "All Consumer Groups",
-            },
-          ]
-        }
-      >
-        test
-      </Breadcrumb>
-    </div>
-  </div>
-  <PageLoader />
-  <ConfirmationModal
-    isOpen={false}
-    onCancel={[Function]}
-    onConfirm={[Function]}
-  >
-    Are you sure you want to delete this consumer group?
-  </ConfirmationModal>
-</div>
-`;
-
-exports[`Details component when consumer gruops are fetched Matches the snapshot 1`] = `
-<div
-  className="section"
->
-  <div
-    className="level"
-  >
-    <div
-      className="level-item level-left"
-    >
-      <Breadcrumb
-        links={
-          Array [
-            Object {
-              "href": "/ui/clusters/local/consumer-groups",
-              "label": "All Consumer Groups",
-            },
-          ]
-        }
-      >
-        test
-      </Breadcrumb>
-    </div>
-  </div>
-  <div
-    className="box"
-  >
-    <div
-      className="level"
-    >
-      <div
-        className="level-item level-right buttons"
-      >
-        <button
-          className="button"
-          onClick={[Function]}
-          type="button"
-        >
-          Reset offsets
-        </button>
-        <button
-          className="button is-danger"
-          onClick={[Function]}
-          type="button"
-        >
-          Delete consumer group
-        </button>
-      </div>
-    </div>
-    <table
-      className="table is-striped is-fullwidth"
-    >
-      <thead>
-        <tr>
-          <th>
-            Consumer ID
-          </th>
-          <th>
-            Host
-          </th>
-          <th>
-            Topic
-          </th>
-          <th>
-            Partition
-          </th>
-          <th>
-            Messages behind
-          </th>
-          <th>
-            Current offset
-          </th>
-          <th>
-            End offset
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <ListItem
-          clusterName="local"
-          consumer={
-            Object {
-              "consumerId": "consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0",
-              "currentOffset": 394,
-              "endOffset": 394,
-              "host": "/172.31.9.153",
-              "messagesBehind": 0,
-              "partition": 6,
-              "topic": "messages",
-            }
-          }
-          key="consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0"
-        />
-        <ListItem
-          clusterName="local"
-          consumer={
-            Object {
-              "consumerId": "consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1",
-              "currentOffset": 384,
-              "endOffset": 384,
-              "host": "/172.31.9.153",
-              "messagesBehind": 0,
-              "partition": 7,
-              "topic": "messages",
-            }
-          }
-          key="consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1"
-        />
-      </tbody>
-    </table>
-  </div>
-  <ConfirmationModal
-    isOpen={false}
-    onCancel={[Function]}
-    onConfirm={[Function]}
-  >
-    Are you sure you want to delete this consumer group?
-  </ConfirmationModal>
-</div>
-`;

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

@@ -1,74 +1,62 @@
 import React from 'react';
-import { ClusterName } from 'redux/interfaces';
-import { ConsumerGroup } from 'generated-sources';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+import { Table } from 'components/common/table/Table/Table.styled';
+import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import Search from 'components/common/Search/Search';
+import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
+import { useAppSelector } from 'lib/hooks/redux';
+import { selectAll } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 
 import ListItem from './ListItem';
 
-interface Props {
-  clusterName: ClusterName;
-  consumerGroups: ConsumerGroup[];
-}
-
-const List: React.FC<Props> = ({ consumerGroups }) => {
+const List: React.FC = () => {
+  const consumerGroups = useAppSelector(selectAll);
   const [searchText, setSearchText] = React.useState<string>('');
 
-  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    setSearchText(event.target.value);
+  const handleInputChange = (search: string) => {
+    setSearchText(search);
   };
 
   return (
-    <div className="section">
-      <Breadcrumb>All Consumer Groups</Breadcrumb>
-
-      <div className="box">
-        <div>
-          <div className="columns">
-            <div className="column is-half is-offset-half">
-              <input
-                id="searchText"
-                type="text"
-                name="searchText"
-                className="input"
-                placeholder="Search"
-                value={searchText}
-                onChange={handleInputChange}
+    <div>
+      <PageHeading text="Consumers" />
+      <ControlPanelWrapper hasInput>
+        <Search
+          placeholder="Search"
+          value={searchText}
+          handleSearch={handleInputChange}
+        />
+      </ControlPanelWrapper>
+      <Table isFullwidth>
+        <thead>
+          <tr>
+            <TableHeaderCell title="Consumer group ID" />
+            <TableHeaderCell title="Num of members" />
+            <TableHeaderCell title="Num of topics" />
+            <TableHeaderCell title="Messages behind" />
+            <TableHeaderCell title="Coordinator" />
+            <TableHeaderCell title="State" />
+          </tr>
+        </thead>
+        <tbody>
+          {consumerGroups
+            .filter(
+              (consumerGroup) =>
+                !searchText || consumerGroup?.groupId?.indexOf(searchText) >= 0
+            )
+            .map((consumerGroup) => (
+              <ListItem
+                key={consumerGroup.groupId}
+                consumerGroup={consumerGroup}
               />
-            </div>
-          </div>
-          <table className="table is-striped is-fullwidth is-hoverable">
-            <thead>
-              <tr>
-                <th>Consumer group ID</th>
-                <th>Num of members</th>
-                <th>Num of topics</th>
-                <th>Messages behind</th>
-                <th>Coordinator</th>
-                <th>State</th>
-              </tr>
-            </thead>
-            <tbody>
-              {consumerGroups
-                .filter(
-                  (consumerGroup) =>
-                    !searchText ||
-                    consumerGroup?.groupId?.indexOf(searchText) >= 0
-                )
-                .map((consumerGroup) => (
-                  <ListItem
-                    key={consumerGroup.groupId}
-                    consumerGroup={consumerGroup}
-                  />
-                ))}
-              {consumerGroups.length === 0 && (
-                <tr>
-                  <td colSpan={10}>No active consumer groups</td>
-                </tr>
-              )}
-            </tbody>
-          </table>
-        </div>
-      </div>
+            ))}
+          {consumerGroups.length === 0 && (
+            <tr>
+              <td colSpan={10}>No active consumer groups</td>
+            </tr>
+          )}
+        </tbody>
+      </Table>
     </div>
   );
 };

+ 0 - 26
kafka-ui-react-app/src/components/ConsumerGroups/List/ListContainer.ts

@@ -1,26 +0,0 @@
-import { connect } from 'react-redux';
-import { ClusterName, RootState } from 'redux/interfaces';
-import { getConsumerGroupsList } from 'redux/reducers/consumerGroups/selectors';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-
-import List from './List';
-
-interface RouteProps {
-  clusterName: ClusterName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { clusterName },
-    },
-  }: OwnProps
-) => ({
-  clusterName,
-  consumerGroups: getConsumerGroupsList(state),
-});
-
-export default withRouter(connect(mapStateToProps)(List));

+ 10 - 11
kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx

@@ -1,26 +1,25 @@
 import React from 'react';
-import { useHistory } from 'react-router-dom';
+import { Link } from 'react-router-dom';
 import { ConsumerGroup } from 'generated-sources';
-import ConsumerGroupStateTag from 'components/common/ConsumerGroupState/ConsumerGroupStateTag';
+import TagStyled from 'components/common/Tag/Tag.styled';
+import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
 
 const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({
   consumerGroup,
 }) => {
-  const history = useHistory();
-
-  function goToConsumerGroupDetails() {
-    history.push(`consumer-groups/${consumerGroup.groupId}`);
-  }
-
   return (
-    <tr className="is-clickable" onClick={goToConsumerGroupDetails}>
-      <td>{consumerGroup.groupId}</td>
+    <tr>
+      <TableKeyLink>
+        <Link to={`consumer-groups/${consumerGroup.groupId}`}>
+          {consumerGroup.groupId}
+        </Link>
+      </TableKeyLink>
       <td>{consumerGroup.members}</td>
       <td>{consumerGroup.topics}</td>
       <td>{consumerGroup.messagesBehind}</td>
       <td>{consumerGroup.coordinator?.id}</td>
       <td>
-        <ConsumerGroupStateTag state={consumerGroup.state} />
+        <TagStyled color="yellow">{consumerGroup.state}</TagStyled>
       </td>
     </tr>
   );

+ 36 - 36
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx

@@ -1,46 +1,46 @@
 import React from 'react';
-import { shallow, mount } from 'enzyme';
 import List from 'components/ConsumerGroups/List/List';
+import { screen } from '@testing-library/react';
+import { StaticRouter } from 'react-router';
+import userEvent from '@testing-library/user-event';
+import { render } from 'lib/testHelpers';
+import { store } from 'redux/store';
+import { fetchConsumerGroups } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
+import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures';
 
 describe('List', () => {
-  const mockConsumerGroups = [
-    {
-      groupId: 'groupId',
-      members: 0,
-      topics: 1,
-      simple: false,
-      partitionAssignor: '',
-      coordinator: {
-        id: 1,
-        host: 'host',
-      },
-      partitions: [
-        {
-          consumerId: null,
-          currentOffset: 0,
-          endOffset: 0,
-          host: null,
-          messagesBehind: 0,
-          partition: 1,
-          topic: 'topic',
-        },
-      ],
-    },
-  ];
-  const component = shallow(
-    <List consumerGroups={mockConsumerGroups} clusterName="cluster" />
-  );
-  const componentEmpty = mount(
-    <List consumerGroups={[]} clusterName="cluster" />
+  beforeEach(() =>
+    render(
+      <StaticRouter>
+        <List />
+      </StaticRouter>
+    )
   );
 
-  it('render empty List consumer Groups', () => {
-    expect(componentEmpty.find('td').text()).toEqual(
-      'No active consumer groups'
-    );
+  it('renders empty table', () => {
+    expect(screen.getByRole('table')).toBeInTheDocument();
+    expect(screen.getByText('No active consumer groups')).toBeInTheDocument();
   });
 
-  it('render List consumer Groups', () => {
-    expect(component.exists('.section')).toBeTruthy();
+  describe('consumerGroups are fecthed', () => {
+    beforeEach(() => {
+      store.dispatch({
+        type: fetchConsumerGroups.fulfilled.type,
+        payload: consumerGroups,
+      });
+    });
+
+    it('renders all rows with consumers', () => {
+      expect(screen.getByText('groupId1')).toBeInTheDocument();
+      expect(screen.getByText('groupId2')).toBeInTheDocument();
+    });
+
+    describe('when searched', () => {
+      it('renders only searched consumers', () => {
+        userEvent.type(screen.getByPlaceholderText('Search'), 'groupId1');
+        expect(screen.getByText('groupId1')).toBeInTheDocument();
+        expect(screen.getByText('groupId2')).toBeInTheDocument();
+      });
+    });
   });
 });

+ 0 - 8
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListContainer.spec.tsx

@@ -1,8 +0,0 @@
-import React from 'react';
-import { containerRendersView } from 'lib/testHelpers';
-import ListContainer from 'components/ConsumerGroups/List/ListContainer';
-import List from 'components/ConsumerGroups/List/List';
-
-describe('ListContainer', () => {
-  containerRendersView(<ListContainer />, List);
-});

+ 16 - 3
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx

@@ -1,6 +1,9 @@
 import React from 'react';
-import { shallow } from 'enzyme';
+import { mount } from 'enzyme';
 import ListItem from 'components/ConsumerGroups/List/ListItem';
+import { ThemeProvider } from 'styled-components';
+import theme from 'theme/theme';
+import { StaticRouter } from 'react-router';
 
 describe('List', () => {
   const mockConsumerGroup = {
@@ -25,9 +28,19 @@ describe('List', () => {
       },
     ],
   };
-  const component = shallow(<ListItem consumerGroup={mockConsumerGroup} />);
+  const component = mount(
+    <StaticRouter>
+      <ThemeProvider theme={theme}>
+        <table>
+          <tbody>
+            <ListItem consumerGroup={mockConsumerGroup} />
+          </tbody>
+        </table>
+      </ThemeProvider>
+    </StaticRouter>
+  );
 
   it('render empty ListItem', () => {
-    expect(component.exists('.is-clickable')).toBeTruthy();
+    expect(component.exists('tr')).toBeTruthy();
   });
 });

+ 0 - 78
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx

@@ -1,78 +0,0 @@
-import React from 'react';
-import { NavLink } from 'react-router-dom';
-import { clusterBrokersPath, clusterTopicsPath } from 'lib/paths';
-import { Cluster, ServerStatus } from 'generated-sources';
-import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
-
-interface ClusterWidgetProps {
-  cluster: Cluster;
-}
-
-const ClusterWidget: React.FC<ClusterWidgetProps> = ({
-  cluster: {
-    name,
-    status,
-    topicCount,
-    brokerCount,
-    bytesInPerSec,
-    bytesOutPerSec,
-    onlinePartitionCount,
-    readOnly,
-    version,
-  },
-}) => (
-  <div className="column is-full-modile is-6">
-    <div className="box">
-      <div className="title is-6 has-text-overflow-ellipsis">
-        <div
-          className={`tag mr-2 ${
-            status === ServerStatus.ONLINE ? 'is-success' : 'is-danger'
-          }`}
-        >
-          {status}
-        </div>
-        {readOnly && <div className="tag mr-2 is-info is-light">readonly</div>}
-        {name}
-      </div>
-
-      <table className="table is-fullwidth">
-        <tbody>
-          <tr>
-            <th>Version</th>
-            <td>{version}</td>
-          </tr>
-          <tr>
-            <th>Brokers</th>
-            <td>
-              <NavLink to={clusterBrokersPath(name)}>{brokerCount}</NavLink>
-            </td>
-          </tr>
-          <tr>
-            <th>Partitions</th>
-            <td>{onlinePartitionCount}</td>
-          </tr>
-          <tr>
-            <th>Topics</th>
-            <td>
-              <NavLink to={clusterTopicsPath(name)}>{topicCount}</NavLink>
-            </td>
-          </tr>
-          <tr>
-            <th>Production</th>
-            <td>
-              <BytesFormatted value={bytesInPerSec} />
-            </td>
-          </tr>
-          <tr>
-            <th>Consumption</th>
-            <td>
-              <BytesFormatted value={bytesOutPerSec} />
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-  </div>
-);
-
-export default ClusterWidget;

+ 68 - 33
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx

@@ -1,11 +1,15 @@
 import React from 'react';
 import { chunk } from 'lodash';
 import { v4 } from 'uuid';
-import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
-import Indicator from 'components/common/Dashboard/Indicator';
+import * as Metrics from 'components/common/Metrics';
 import { Cluster } from 'generated-sources';
-
-import ClusterWidget from './ClusterWidget';
+import TagStyled from 'components/common/Tag/Tag.styled';
+import { Table } from 'components/common/table/Table/Table.styled';
+import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
+import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
+import { NavLink } from 'react-router-dom';
+import { clusterTopicsPath } from 'lib/paths';
+import Switch from 'components/common/Switch/Switch';
 
 interface Props {
   clusters: Cluster[];
@@ -41,37 +45,68 @@ const ClustersWidget: React.FC<Props> = ({
   const handleSwitch = () => setShowOfflineOnly(!showOfflineOnly);
 
   return (
-    <div>
-      <h5 className="title is-5">Clusters</h5>
-
-      <MetricsWrapper>
-        <Indicator label="Online Clusters">
-          <span className="tag is-success">{onlineClusters.length}</span>
-        </Indicator>
-        <Indicator label="Offline Clusters">
-          <span className="tag is-danger">{offlineClusters.length}</span>
-        </Indicator>
-        <Indicator label="Hide online clusters">
-          <input
-            type="checkbox"
-            className="switch is-rounded"
-            name="switchRoundedDefault"
-            id="switchRoundedDefault"
-            checked={showOfflineOnly}
-            onChange={handleSwitch}
-          />
-          <label htmlFor="switchRoundedDefault" />
-        </Indicator>
-      </MetricsWrapper>
-
+    <>
+      <Metrics.Wrapper>
+        <Metrics.Section>
+          <Metrics.Indicator
+            label={<TagStyled color="green">Online</TagStyled>}
+          >
+            <span data-testid="onlineCount">{onlineClusters.length}</span>{' '}
+            <Metrics.LightText>clusters</Metrics.LightText>
+          </Metrics.Indicator>
+          <Metrics.Indicator
+            label={<TagStyled color="gray">Offline</TagStyled>}
+          >
+            <span data-testid="offlineCount">{offlineClusters.length}</span>{' '}
+            <Metrics.LightText>clusters</Metrics.LightText>
+          </Metrics.Indicator>
+        </Metrics.Section>
+      </Metrics.Wrapper>
+      <div className="p-4">
+        <Switch
+          name="switchRoundedDefault"
+          checked={showOfflineOnly}
+          onChange={handleSwitch}
+        />
+        <span>Only offline clusters</span>
+      </div>
       {clusterList.map((chunkItem) => (
-        <div className="columns" key={chunkItem.id}>
-          {chunkItem.data.map((cluster) => (
-            <ClusterWidget cluster={cluster} key={cluster.name} />
-          ))}
-        </div>
+        <Table key={chunkItem.id} isFullwidth>
+          <thead>
+            <tr>
+              <TableHeaderCell title="Cluster name" />
+              <TableHeaderCell title="Version" />
+              <TableHeaderCell title="Brokers count" />
+              <TableHeaderCell title="Partitions" />
+              <TableHeaderCell title="Topics" />
+              <TableHeaderCell title="Production" />
+              <TableHeaderCell title="Consumption" />
+            </tr>
+          </thead>
+          <tbody>
+            {chunkItem.data.map((cluster) => (
+              <tr key={cluster.name}>
+                <td>{cluster.name}</td>
+                <td>{cluster.version}</td>
+                <td>{cluster.brokerCount}</td>
+                <td>{cluster.onlinePartitionCount}</td>
+                <td>
+                  <NavLink to={clusterTopicsPath(cluster.name)}>
+                    {cluster.topicCount}
+                  </NavLink>
+                </td>
+                <td>
+                  <BytesFormatted value={cluster.bytesInPerSec} />
+                </td>
+                <td>
+                  <BytesFormatted value={cluster.bytesOutPerSec} />
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </Table>
       ))}
-    </div>
+    </>
   );
 };
 

+ 1 - 1
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts

@@ -3,7 +3,7 @@ import {
   getClusterList,
   getOnlineClusters,
   getOfflineClusters,
-} from 'redux/reducers/clusters/selectors';
+} from 'redux/reducers/clusters/clustersSlice';
 import { RootState } from 'redux/interfaces';
 
 import ClustersWidget from './ClustersWidget';

+ 0 - 87
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx

@@ -1,87 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { ServerStatus } from 'generated-sources';
-import { clusterBrokersPath, clusterTopicsPath } from 'lib/paths';
-import ClusterWidget from 'components/Dashboard/ClustersWidget/ClusterWidget';
-
-import { offlineCluster, onlineCluster } from './fixtures';
-
-describe('ClusterWidget', () => {
-  describe('when cluster is online', () => {
-    it('renders with correct tag', () => {
-      const tag = shallow(<ClusterWidget cluster={onlineCluster} />).find(
-        '.tag'
-      );
-      expect(tag.hasClass('is-success')).toBeTruthy();
-      expect(tag.text()).toEqual(ServerStatus.ONLINE);
-    });
-
-    it('renders table', () => {
-      const table = shallow(<ClusterWidget cluster={onlineCluster} />).find(
-        'table'
-      );
-      expect(table.hasClass('is-fullwidth')).toBeTruthy();
-
-      expect(
-        table.find(`NavLink[to="${clusterBrokersPath(onlineCluster.name)}"]`)
-          .exists
-      ).toBeTruthy();
-      expect(
-        table.find(`NavLink[to="${clusterTopicsPath(onlineCluster.name)}"]`)
-          .exists
-      ).toBeTruthy();
-    });
-
-    it('matches snapshot', () => {
-      expect(
-        shallow(<ClusterWidget cluster={onlineCluster} />)
-      ).toMatchSnapshot();
-    });
-  });
-
-  describe('when cluster is offline', () => {
-    it('renders with correct tag', () => {
-      const tag = shallow(<ClusterWidget cluster={offlineCluster} />).find(
-        '.tag'
-      );
-
-      expect(tag.hasClass('is-danger')).toBeTruthy();
-      expect(tag.text()).toEqual(ServerStatus.OFFLINE);
-    });
-
-    it('renders table', () => {
-      const table = shallow(<ClusterWidget cluster={offlineCluster} />).find(
-        'table'
-      );
-      expect(table.hasClass('is-fullwidth')).toBeTruthy();
-
-      expect(
-        table.find(`NavLink[to="${clusterBrokersPath(onlineCluster.name)}"]`)
-          .exists
-      ).toBeTruthy();
-      expect(
-        table.find(`NavLink[to="${clusterTopicsPath(onlineCluster.name)}"]`)
-          .exists
-      ).toBeTruthy();
-    });
-
-    it('matches snapshot', () => {
-      expect(
-        shallow(<ClusterWidget cluster={offlineCluster} />)
-      ).toMatchSnapshot();
-    });
-  });
-
-  describe('when cluster is read-only', () => {
-    it('renders the tag', () => {
-      expect(
-        shallow(
-          <ClusterWidget cluster={{ ...onlineCluster, readOnly: true }} />
-        )
-          .find('.title')
-          .childAt(1)
-          .text()
-      ).toEqual('readonly');
-    });
-  });
-});

+ 19 - 23
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx

@@ -1,37 +1,33 @@
 import React from 'react';
-import { shallow } from 'enzyme';
+import { StaticRouter } from 'react-router';
+import { screen } from '@testing-library/react';
 import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
+import userEvent from '@testing-library/user-event';
+import { render } from 'lib/testHelpers';
 
 import { offlineCluster, onlineCluster, clusters } from './fixtures';
 
-const component = () =>
-  shallow(
-    <ClustersWidget
-      clusters={clusters}
-      onlineClusters={[onlineCluster]}
-      offlineClusters={[offlineCluster]}
-    />
+const setupComponent = () =>
+  render(
+    <StaticRouter>
+      <ClustersWidget
+        clusters={clusters}
+        onlineClusters={[onlineCluster]}
+        offlineClusters={[offlineCluster]}
+      />
+    </StaticRouter>
   );
 
 describe('ClustersWidget', () => {
-  it('renders clusterWidget list', () => {
-    const clusterWidget = component().find('ClusterWidget');
-    expect(clusterWidget.length).toBe(2);
-  });
+  beforeEach(() => setupComponent());
 
-  it('renders ClusterWidget', () => {
-    expect(component().exists('ClusterWidget')).toBeTruthy();
-  });
-
-  it('renders columns', () => {
-    expect(component().exists('.columns')).toBeTruthy();
+  it('renders clusterWidget list', () => {
+    expect(screen.getAllByRole('row').length).toBe(3);
   });
 
   it('hides online cluster widgets', () => {
-    const value = component();
-    const input = value.find('input');
-    expect(value.find('ClusterWidget').length).toBe(2);
-    input.simulate('change', { target: { checked: true } });
-    expect(value.find('ClusterWidget').length).toBe(1);
+    expect(screen.getAllByRole('row').length).toBe(3);
+    userEvent.click(screen.getByRole('checkbox'));
+    expect(screen.getAllByRole('row').length).toBe(2);
   });
 });

+ 10 - 2
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidgetContainer.spec.tsx

@@ -3,16 +3,24 @@ import { mount } from 'enzyme';
 import { containerRendersView } from 'lib/testHelpers';
 import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
 import ClustersWidgetContainer from 'components/Dashboard/ClustersWidget/ClustersWidgetContainer';
+import theme from 'theme/theme';
+import { ThemeProvider } from 'styled-components';
 
 describe('ClustersWidgetContainer', () => {
   containerRendersView(<ClustersWidgetContainer />, ClustersWidget);
   describe('view empty ClusterWidget', () => {
     const setupEmptyWrapper = () => (
-      <ClustersWidget clusters={[]} onlineClusters={[]} offlineClusters={[]} />
+      <ThemeProvider theme={theme}>
+        <ClustersWidget
+          clusters={[]}
+          onlineClusters={[]}
+          offlineClusters={[]}
+        />
+      </ThemeProvider>
     );
     it(' is empty when no online clusters', () => {
       const wrapper = mount(setupEmptyWrapper());
-      expect(wrapper.find('.is-success').text()).toBe('0');
+      expect(wrapper.find('[data-testid="onlineCount"]').text()).toBe('0');
     });
   });
 });

+ 0 - 171
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/__snapshots__/ClusterWidget.spec.tsx.snap

@@ -1,171 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ClusterWidget when cluster is offline matches snapshot 1`] = `
-<div
-  className="column is-full-modile is-6"
->
-  <div
-    className="box"
-  >
-    <div
-      className="title is-6 has-text-overflow-ellipsis"
-    >
-      <div
-        className="tag mr-2 is-danger"
-      >
-        offline
-      </div>
-      local
-    </div>
-    <table
-      className="table is-fullwidth"
-    >
-      <tbody>
-        <tr>
-          <th>
-            Version
-          </th>
-          <td />
-        </tr>
-        <tr>
-          <th>
-            Brokers
-          </th>
-          <td>
-            <NavLink
-              to="/ui/clusters/local/brokers"
-            >
-              1
-            </NavLink>
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Partitions
-          </th>
-          <td>
-            2
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Topics
-          </th>
-          <td>
-            <NavLink
-              to="/ui/clusters/local/topics"
-            >
-              2
-            </NavLink>
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Production
-          </th>
-          <td>
-            <BytesFormatted
-              value={8000.00000673768}
-            />
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Consumption
-          </th>
-          <td>
-            <BytesFormatted
-              value={0.815306356729712}
-            />
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-</div>
-`;
-
-exports[`ClusterWidget when cluster is online matches snapshot 1`] = `
-<div
-  className="column is-full-modile is-6"
->
-  <div
-    className="box"
-  >
-    <div
-      className="title is-6 has-text-overflow-ellipsis"
-    >
-      <div
-        className="tag mr-2 is-success"
-      >
-        online
-      </div>
-      secondLocal
-    </div>
-    <table
-      className="table is-fullwidth"
-    >
-      <tbody>
-        <tr>
-          <th>
-            Version
-          </th>
-          <td />
-        </tr>
-        <tr>
-          <th>
-            Brokers
-          </th>
-          <td>
-            <NavLink
-              to="/ui/clusters/secondLocal/brokers"
-            >
-              1
-            </NavLink>
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Partitions
-          </th>
-          <td>
-            6
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Topics
-          </th>
-          <td>
-            <NavLink
-              to="/ui/clusters/secondLocal/topics"
-            >
-              3
-            </NavLink>
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Production
-          </th>
-          <td>
-            <BytesFormatted
-              value={0.00003061819685376472}
-            />
-          </td>
-        </tr>
-        <tr>
-          <th>
-            Consumption
-          </th>
-          <td>
-            <BytesFormatted
-              value={5.737800890036267}
-            />
-          </td>
-        </tr>
-      </tbody>
-    </table>
-  </div>
-</div>
-`;

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

@@ -1,16 +1,9 @@
 import React from 'react';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 
 import ClustersWidgetContainer from './ClustersWidget/ClustersWidgetContainer';
 
 const Dashboard: React.FC = () => (
-  <div className="section">
-    <div className="level">
-      <div className="level-item level-left">
-        <Breadcrumb>Dashboard</Breadcrumb>
-      </div>
-    </div>
-
+  <div>
     <ClustersWidgetContainer />
   </div>
 );

+ 0 - 4
kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx

@@ -5,10 +5,6 @@ import Dashboard from 'components/Dashboard/Dashboard';
 const component = shallow(<Dashboard />);
 
 describe('Dashboard', () => {
-  it('renders section', () => {
-    expect(component.exists('.section')).toBe(true);
-  });
-
   it('renders ClustersWidget', () => {
     expect(component.exists('Connect(ClustersWidget)')).toBe(true);
   });

+ 0 - 30
kafka-ui-react-app/src/components/KsqlDb/BreadCrumbs/BreadCrumbs.tsx

@@ -1,30 +0,0 @@
-import React from 'react';
-import Breadcrumb, {
-  BreadcrumbItem,
-} from 'components/common/Breadcrumb/Breadcrumb';
-import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
-import { useParams, useRouteMatch } from 'react-router';
-
-interface RouteParams {
-  clusterName: string;
-}
-
-const Breadcrumbs: React.FC = () => {
-  const { clusterName } = useParams<RouteParams>();
-  const isQuery = useRouteMatch(clusterKsqlDbQueryPath(clusterName));
-
-  if (!isQuery) {
-    return <Breadcrumb>KSQLDB</Breadcrumb>;
-  }
-
-  const links: BreadcrumbItem[] = [
-    {
-      label: 'KSQLDB',
-      href: clusterKsqlDbPath(clusterName),
-    },
-  ];
-
-  return <Breadcrumb links={links}>Query</Breadcrumb>;
-};
-
-export default Breadcrumbs;

+ 0 - 33
kafka-ui-react-app/src/components/KsqlDb/BreadCrumbs/__test__/BreadCrumbs.spec.tsx

@@ -1,33 +0,0 @@
-import React from 'react';
-import { StaticRouter } from 'react-router';
-import Breadcrumbs from 'components/KsqlDb/BreadCrumbs/BreadCrumbs';
-import { mount } from 'enzyme';
-import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
-
-describe('BreadCrumbs', () => {
-  const clusterName = 'local';
-  const rootPathname = clusterKsqlDbPath(clusterName);
-  const queryPathname = clusterKsqlDbQueryPath(clusterName);
-
-  const setupComponent = (pathname: string) => (
-    <StaticRouter location={{ pathname }} context={{}}>
-      <Breadcrumbs />
-    </StaticRouter>
-  );
-
-  it('Renders root path', () => {
-    const component = mount(setupComponent(rootPathname));
-
-    expect(component.find({ children: 'KSQLDB' }).exists()).toBeTruthy();
-    expect(component.find({ children: 'Query' }).exists()).toBeFalsy();
-  });
-
-  it('Renders query path', () => {
-    const component = mount(setupComponent(queryPathname));
-
-    expect(
-      component.find('a').find({ children: 'KSQLDB' }).exists()
-    ).toBeTruthy();
-    expect(component.find({ children: 'Query' }).exists()).toBeTruthy();
-  });
-});

+ 4 - 10
kafka-ui-react-app/src/components/KsqlDb/KsqlDb.tsx

@@ -3,19 +3,13 @@ import { Switch, Route } from 'react-router-dom';
 import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
 import List from 'components/KsqlDb/List/List';
 import Query from 'components/KsqlDb/Query/Query';
-import Breadcrumbs from 'components/KsqlDb/BreadCrumbs/BreadCrumbs';
 
 const KsqlDb: React.FC = () => {
   return (
-    <div className="section">
-      <Switch>
-        <Route path={clusterKsqlDbPath()} component={Breadcrumbs} />
-      </Switch>
-      <Switch>
-        <Route exact path={clusterKsqlDbPath()} component={List} />
-        <Route exact path={clusterKsqlDbQueryPath()} component={Query} />
-      </Switch>
-    </div>
+    <Switch>
+      <Route exact path={clusterKsqlDbPath()} component={List} />
+      <Route exact path={clusterKsqlDbQueryPath()} component={Query} />
+    </Switch>
   );
 };
 

+ 30 - 28
kafka-ui-react-app/src/components/KsqlDb/List/List.tsx

@@ -1,5 +1,4 @@
-import Indicator from 'components/common/Dashboard/Indicator';
-import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
+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';
@@ -7,8 +6,11 @@ import { useDispatch, useSelector } from 'react-redux';
 import { useParams } from 'react-router';
 import { fetchKsqlDbTables } from 'redux/actions/thunks/ksqlDb';
 import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
-import { Link } from 'react-router-dom';
 import { clusterKsqlDbQueryPath } 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';
+import { Button } from 'components/common/Button/Button';
 
 const headers = [
   { Header: 'Type', accessor: 'type' },
@@ -34,42 +36,40 @@ const List: FC = () => {
 
   return (
     <>
-      <MetricsWrapper wrapperClassName="is-justify-content-space-between">
-        <div className="column is-flex m-0 p-0">
-          <Indicator
-            className="level-left is-one-third mr-3"
-            label="Tables"
-            title="Tables"
-            fetching={fetching}
-          >
+      <PageHeading text="KSQL DB">
+        <Button
+          isLink
+          to={clusterKsqlDbQueryPath(clusterName)}
+          buttonType="primary"
+          buttonSize="M"
+        >
+          Execute KSQL request
+        </Button>
+      </PageHeading>
+      <Metrics.Wrapper>
+        <Metrics.Section>
+          <Metrics.Indicator label="Tables" title="Tables" fetching={fetching}>
             {tablesCount}
-          </Indicator>
-          <Indicator
-            className="level-left is-one-third ml-3"
+          </Metrics.Indicator>
+          <Metrics.Indicator
             label="Streams"
             title="Streams"
             fetching={fetching}
           >
             {streamsCount}
-          </Indicator>
-        </div>
-        <Link
-          to={clusterKsqlDbQueryPath(clusterName)}
-          className="button is-primary"
-        >
-          Execute ksql
-        </Link>
-      </MetricsWrapper>
-      <div className="box">
+          </Metrics.Indicator>
+        </Metrics.Section>
+      </Metrics.Wrapper>
+      <div>
         {fetching ? (
           <PageLoader />
         ) : (
-          <table className="table is-fullwidth">
+          <Table isFullwidth>
             <thead>
               <tr>
                 <th> </th>
                 {headers.map(({ Header, accessor }) => (
-                  <th key={accessor}>{Header}</th>
+                  <TableHeaderCell title={Header} key={accessor} />
                 ))}
               </tr>
             </thead>
@@ -79,11 +79,13 @@ const List: FC = () => {
               ))}
               {rows.length === 0 && (
                 <tr>
-                  <td colSpan={headers.length}>No tables or streams found</td>
+                  <td colSpan={headers.length + 1}>
+                    No tables or streams found
+                  </td>
                 </tr>
               )}
             </tbody>
-          </table>
+          </Table>
         )}
       </div>
     </>

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

@@ -1,3 +1,5 @@
+import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
+import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';
 import React from 'react';
 
 interface Props {
@@ -16,13 +18,9 @@ const ListItem: React.FC<Props> = ({ accessors, data }) => {
     <>
       <tr>
         <td>
-          <span
-            className="icon has-text-link is-size-7 is-small is-clickable"
-            onClick={toggleIsOpen}
-            aria-hidden
-          >
-            <i className={`fas fa-${isOpen ? 'minus' : 'plus'}`} />
-          </span>
+          <IconButtonWrapper onClick={toggleIsOpen}>
+            <MessageToggleIcon isOpen={isOpen} />
+          </IconButtonWrapper>
         </td>
         {accessors.map((accessor) => (
           <td key={accessor}>{data[accessor]}</td>

+ 29 - 55
kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx

@@ -1,63 +1,37 @@
 import React from 'react';
 import List from 'components/KsqlDb/List/List';
-import { mount } from 'enzyme';
-import { StaticRouter } from 'react-router';
-import configureStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixtures';
-
-const emptyPlaceholder = 'No tables or streams found';
-
-const mockStore = configureStore();
+import { Route, Router } from 'react-router';
+import { createMemoryHistory } from 'history';
+import { clusterKsqlDbPath } from 'lib/paths';
+import { render } 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>
+  );
+};
 
 describe('KsqlDb List', () => {
-  const pathname = `ui/clusters/local/ksql-db`;
-
-  it('Renders placeholder on empty data', () => {
-    const initialState: Partial<RootState> = {
-      ksqlDb: {
-        tables: [],
-        streams: [],
-        executionResult: null,
-      },
-      loader: {
-        GET_KSQL_DB_TABLES_AND_STREAMS: 'fetched',
-      },
-    };
-    const store = mockStore(initialState);
-
-    const component = mount(
-      <StaticRouter location={{ pathname }} context={{}}>
-        <Provider store={store}>
-          <List />
-        </Provider>
-      </StaticRouter>
-    );
-
-    expect(
-      component.find({ children: emptyPlaceholder }).exists()
-    ).toBeTruthy();
-  });
-
-  it('Renders rows', () => {
-    const initialState: Partial<RootState> = {
-      ksqlDb: { ...fetchKsqlDbTablesPayload, executionResult: null },
-      loader: {
-        GET_KSQL_DB_TABLES_AND_STREAMS: 'fetched',
+  afterEach(() => fetchMock.reset());
+  it('renders placeholder on empty data', async () => {
+    fetchMock.post(
+      {
+        url: `/api/clusters/${clusterName}/ksql`,
       },
-    };
-    const store = mockStore(initialState);
-
-    const component = mount(
-      <StaticRouter location={{ pathname }} context={{}}>
-        <Provider store={store}>
-          <List />
-        </Provider>
-      </StaticRouter>
+      { data: [] }
     );
-
-    // 2 streams, 2 tables and 1 head tr
-    expect(component.find('tr').length).toEqual(5);
+    renderComponent();
+    await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
+    expect(screen.getByText('No tables or streams found')).toBeInTheDocument();
   });
 });

+ 26 - 0
kafka-ui-react-app/src/components/KsqlDb/Query/Query.styled.ts

@@ -0,0 +1,26 @@
+import styled from 'styled-components';
+
+export const QueryWrapper = styled.div`
+  padding: 16px;
+`;
+
+export const KSQLInputsWrapper = styled.div`
+  width: 100%;
+  display: flex;
+  gap: 24px;
+
+  padding-bottom: 16px;
+  & > div {
+    flex-grow: 1;
+  }
+`;
+
+export const KSQLInputHeader = styled.div`
+  display: flex;
+  justify-content: space-between;
+`;
+
+export const KSQLButtons = styled.div`
+  display: flex;
+  gap: 16px;
+`;

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.