浏览代码

Kafka Connect with React-Query (#2258)

* Kafka Connect hooks

* Migrate connectors list page to RQ

* Migrate connector details page overview to RQ

* Migrate connector config page to RQ

* Migrate connector tasks page to RQ

* Get rid of some deadcode

* Migrate connector Actions page to RQ

* Get rid of some deadcode

* Migrate connector create page to RQ

* Migrate connector Edit page to RQ

* move fixtures to lib folder

* refactoring
Oleg Shur 2 年之前
父节点
当前提交
9af6b0032b
共有 79 个文件被更改,包括 1686 次插入3634 次删除
  1. 7 1
      kafka-ui-react-app/.prettierrc
  2. 1 0
      kafka-ui-react-app/jest.config.ts
  3. 3 1
      kafka-ui-react-app/package.json
  4. 54 0
      kafka-ui-react-app/pnpm-lock.yaml
  5. 1 1
      kafka-ui-react-app/sonar-project.properties
  6. 1 1
      kafka-ui-react-app/src/components/Alerts/Alert.tsx
  7. 85 74
      kafka-ui-react-app/src/components/App.tsx
  8. 2 2
      kafka-ui-react-app/src/components/Brokers/Broker/Broker.tsx
  9. 2 2
      kafka-ui-react-app/src/components/Brokers/Broker/BrokerLogdir/BrokerLogdir.tsx
  10. 2 2
      kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/BrokerMetrics.tsx
  11. 18 24
      kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/__test__/BrokerMetrics.spec.tsx
  12. 25 26
      kafka-ui-react-app/src/components/Brokers/Broker/__test__/Broker.spec.tsx
  13. 2 2
      kafka-ui-react-app/src/components/Brokers/BrokersList/BrokersList.tsx
  14. 49 75
      kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx
  15. 1 1
      kafka-ui-react-app/src/components/Cluster/Cluster.tsx
  16. 8 8
      kafka-ui-react-app/src/components/Connect/Connect.tsx
  17. 54 95
      kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
  18. 0 32
      kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts
  19. 107 206
      kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx
  20. 12 45
      kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx
  21. 0 20
      kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts
  22. 32 57
      kafka-ui-react-app/src/components/Connect/Details/Config/__test__/Config.spec.tsx
  23. 0 117
      kafka-ui-react-app/src/components/Connect/Details/Details.tsx
  24. 0 28
      kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts
  25. 80 0
      kafka-ui-react-app/src/components/Connect/Details/DetailsPage.tsx
  26. 18 17
      kafka-ui-react-app/src/components/Connect/Details/Overview/Overview.tsx
  27. 0 17
      kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts
  28. 45 28
      kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx
  29. 19 0
      kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/getTaskMetrics.spec.ts
  30. 23 0
      kafka-ui-react-app/src/components/Connect/Details/Overview/getTaskMetrics.ts
  31. 0 56
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx
  32. 0 20
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItemContainer.ts
  33. 0 70
      kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/__tests__/ListItem.spec.tsx
  34. 41 15
      kafka-ui-react-app/src/components/Connect/Details/Tasks/Tasks.tsx
  35. 0 20
      kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts
  36. 29 46
      kafka-ui-react-app/src/components/Connect/Details/Tasks/__tests__/Tasks.spec.tsx
  37. 0 136
      kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx
  38. 84 0
      kafka-ui-react-app/src/components/Connect/Details/__tests__/DetailsPage.spec.tsx
  39. 15 46
      kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx
  40. 0 24
      kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts
  41. 37 41
      kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx
  42. 33 126
      kafka-ui-react-app/src/components/Connect/List/List.tsx
  43. 0 37
      kafka-ui-react-app/src/components/Connect/List/ListContainer.ts
  44. 14 24
      kafka-ui-react-app/src/components/Connect/List/ListItem.tsx
  45. 86 0
      kafka-ui-react-app/src/components/Connect/List/ListPage.tsx
  46. 45 90
      kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx
  47. 1 8
      kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
  48. 182 0
      kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx
  49. 18 38
      kafka-ui-react-app/src/components/Connect/New/New.tsx
  50. 0 20
      kafka-ui-react-app/src/components/Connect/New/NewContainer.ts
  51. 30 52
      kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx
  52. 11 11
      kafka-ui-react-app/src/components/Connect/__tests__/Connect.spec.tsx
  53. 1 1
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx
  54. 1 1
      kafka-ui-react-app/src/components/Nav/Nav.tsx
  55. 0 5
      kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx
  56. 9 1
      kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx
  57. 1 1
      kafka-ui-react-app/src/components/common/Metrics/Section.tsx
  58. 1 6
      kafka-ui-react-app/src/index.tsx
  59. 6 111
      kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts
  60. 168 0
      kafka-ui-react-app/src/lib/hooks/api/__tests__/kafkaConnect.spec.ts
  61. 35 0
      kafka-ui-react-app/src/lib/hooks/api/brokers.ts
  62. 14 0
      kafka-ui-react-app/src/lib/hooks/api/clusters.ts
  63. 128 0
      kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts
  64. 0 8
      kafka-ui-react-app/src/lib/hooks/api/useClusters.ts
  65. 0 11
      kafka-ui-react-app/src/lib/hooks/useBrokers.tsx
  66. 0 18
      kafka-ui-react-app/src/lib/hooks/useBrokersLogDirs.tsx
  67. 0 18
      kafka-ui-react-app/src/lib/hooks/useBrokersMetrics.tsx
  68. 0 11
      kafka-ui-react-app/src/lib/hooks/useClusterStats.tsx
  69. 13 13
      kafka-ui-react-app/src/lib/paths.ts
  70. 31 38
      kafka-ui-react-app/src/lib/testHelpers.tsx
  71. 0 47
      kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts
  72. 0 23
      kafka-ui-react-app/src/redux/interfaces/connect.ts
  73. 0 2
      kafka-ui-react-app/src/redux/interfaces/index.ts
  74. 0 766
      kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts
  75. 0 132
      kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts
  76. 0 478
      kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts
  77. 0 177
      kafka-ui-react-app/src/redux/reducers/connect/selectors.ts
  78. 0 2
      kafka-ui-react-app/src/redux/reducers/index.ts
  79. 1 2
      kafka-ui-react-app/src/theme/theme.ts

+ 7 - 1
kafka-ui-react-app/.prettierrc

@@ -1,4 +1,10 @@
 {
+  "trailingComma": "es5",
+  "semi": true,
   "singleQuote": true,
-  "trailingComma": "es5"
+  "quoteProps": "as-needed",
+  "jsxSingleQuote": false,
+  "bracketSpacing": true,
+  "bracketSameLine": false,
+  "arrowParens": "always"
 }

+ 1 - 0
kafka-ui-react-app/jest.config.ts

@@ -6,6 +6,7 @@ export default {
   coveragePathIgnorePatterns: [
     '/node_modules/',
     '<rootDir>/src/generated-sources/',
+    '<rootDir>/src/lib/fixtures/',
     '<rootDir>/vite.config.ts',
     '<rootDir>/src/index.tsx',
     '<rootDir>/src/serviceWorker.ts',

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

@@ -67,7 +67,8 @@
     "test:CI": "CI=true pnpm test:coverage --ci --testResultsProcessor=\"jest-sonar-reporter\" --watchAll=false",
     "tsc": "tsc --pretty --noEmit",
     "prepare": "cd .. && husky install kafka-ui-react-app/.husky",
-    "pre-commit": "pnpm tsc && lint-staged"
+    "pre-commit": "pnpm tsc && lint-staged",
+    "deadcode": "ts-prune -i src/generated-sources"
   },
   "eslintConfig": {
     "extends": "react-app"
@@ -119,6 +120,7 @@
     "rimraf": "^3.0.2",
     "ts-jest": "^28.0.5",
     "ts-node": "^10.8.1",
+    "ts-prune": "^0.10.3",
     "typescript": "^4.7.4"
   },
   "engines": {

+ 54 - 0
kafka-ui-react-app/pnpm-lock.yaml

@@ -86,6 +86,7 @@ specifiers:
   styled-components: ^5.3.1
   ts-jest: ^28.0.5
   ts-node: ^10.8.1
+  ts-prune: ^0.10.3
   typescript: ^4.7.4
   use-debounce: ^8.0.1
   vite: ^2.9.11
@@ -186,6 +187,7 @@ devDependencies:
   rimraf: 3.0.2
   ts-jest: 28.0.5_c4h4g76dcvkfgjwf6rprlfxfli
   ts-node: 10.8.1_t4lrjbt3sxauai4t5o275zsepa
+  ts-prune: 0.10.3
   typescript: 4.7.4
 
 packages:
@@ -2137,6 +2139,15 @@ packages:
     engines: {node: '>= 10'}
     dev: true
 
+  /@ts-morph/common/0.12.3:
+    resolution: {integrity: sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==}
+    dependencies:
+      fast-glob: 3.2.11
+      minimatch: 3.1.2
+      mkdirp: 1.0.4
+      path-browserify: 1.0.1
+    dev: true
+
   /@tsconfig/node10/1.0.9:
     resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
 
@@ -3169,6 +3180,10 @@ packages:
     resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
     engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
 
+  /code-block-writer/11.0.1:
+    resolution: {integrity: sha512-0ch9DeCY8v/BWA9n1/Qu1ALG3lpesel4PYL2eNlGLgvGl+J7k74i+dSXSF3wLvF5SYII8/GUT/Ic+fycBR/DUQ==}
+    dev: true
+
   /collect-v8-coverage/1.0.1:
     resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
 
@@ -3200,6 +3215,11 @@ packages:
       delayed-stream: 1.0.0
     dev: true
 
+  /commander/6.2.1:
+    resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
+    engines: {node: '>= 6'}
+    dev: true
+
   /commander/8.3.0:
     resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
     engines: {node: '>= 12'}
@@ -5763,6 +5783,12 @@ packages:
   /minimist/1.2.6:
     resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
 
+  /mkdirp/1.0.4:
+    resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
+    engines: {node: '>=10'}
+    hasBin: true
+    dev: true
+
   /ms/2.0.0:
     resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
     dev: true
@@ -6036,6 +6062,10 @@ packages:
     resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
     dev: true
 
+  /path-browserify/1.0.1:
+    resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+    dev: true
+
   /path-exists/3.0.0:
     resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
     engines: {node: '>=4'}
@@ -6973,6 +7003,11 @@ packages:
     hasBin: true
     dev: true
 
+  /true-myth/4.1.1:
+    resolution: {integrity: sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==}
+    engines: {node: 10.* || >= 12.*}
+    dev: true
+
   /ts-jest/28.0.5_c4h4g76dcvkfgjwf6rprlfxfli:
     resolution: {integrity: sha512-Sx9FyP9pCY7pUzQpy4FgRZf2bhHY3za576HMKJFs+OnQ9jS96Du5vNsDKkyedQkik+sEabbKAnCliv9BEsHZgQ==}
     engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
@@ -7005,6 +7040,13 @@ packages:
       yargs-parser: 21.0.1
     dev: true
 
+  /ts-morph/13.0.3:
+    resolution: {integrity: sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==}
+    dependencies:
+      '@ts-morph/common': 0.12.3
+      code-block-writer: 11.0.1
+    dev: true
+
   /ts-node/10.8.1_t4lrjbt3sxauai4t5o275zsepa:
     resolution: {integrity: sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==}
     hasBin: true
@@ -7035,6 +7077,18 @@ packages:
       v8-compile-cache-lib: 3.0.1
       yn: 3.1.1
 
+  /ts-prune/0.10.3:
+    resolution: {integrity: sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==}
+    hasBin: true
+    dependencies:
+      commander: 6.2.1
+      cosmiconfig: 7.0.1
+      json5: 2.2.1
+      lodash: 4.17.21
+      true-myth: 4.1.1
+      ts-morph: 13.0.3
+    dev: true
+
   /tsconfig-paths/3.14.1:
     resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==}
     dependencies:

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

@@ -2,7 +2,7 @@ sonar.projectKey=com.provectus:kafka-ui_frontend
 sonar.organization=provectus
 
 sonar.sources=.
-sonar.exclusions=**/__tests__/**,**/__test__/**,src/serviceWorker.ts,src/setupTests.ts,src/setupProxy.js,**/fixtures.ts,src/lib/testHelpers.tsx,src/index.tsx,vite.config.ts,config/**
+sonar.exclusions=**/__tests__/**,**/__test__/**,src/serviceWorker.ts,src/setupTests.ts,src/setupProxy.js,**/fixtures.ts,src/lib/fixtures/**,src/lib/testHelpers.tsx,src/index.tsx,vite.config.ts,config/**
 
 sonar.typescript.lcov.reportPaths=./coverage/lcov.info
 sonar.testExecutionReportPaths=./test-report.xml

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

@@ -5,7 +5,7 @@ import { Alert as AlertType } from 'redux/interfaces';
 
 import * as S from './Alert.styled';
 
-export interface AlertProps {
+interface AlertProps {
   title: AlertType['title'];
   type: AlertType['type'];
   message: AlertType['message'];

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

@@ -10,12 +10,21 @@ import Version from 'components/Version/Version';
 import Alerts from 'components/Alerts/Alerts';
 import { ThemeProvider } from 'styled-components';
 import theme from 'theme/theme';
+import { QueryClient, QueryClientProvider } from 'react-query';
 
 import * as S from './App.styled';
 import Logo from './common/Logo/Logo';
 import GitIcon from './common/Icons/GitIcon';
 import DiscordIcon from './common/Icons/DiscordIcon';
 
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      suspense: true,
+    },
+  },
+});
+
 const App: React.FC = () => {
   const [isSidebarVisible, setIsSidebarVisible] = React.useState(false);
   const onBurgerClick = () => setIsSidebarVisible(!isSidebarVisible);
@@ -27,87 +36,89 @@ const App: React.FC = () => {
   }, [location, closeSidebar]);
 
   return (
-    <ThemeProvider theme={theme}>
-      <S.Layout>
-        <S.Navbar role="navigation" aria-label="Page Header">
-          <S.NavbarBrand>
+    <QueryClientProvider client={queryClient}>
+      <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}
-                aria-label="burger"
-              >
-                <S.Span role="separator" />
-                <S.Span role="separator" />
-                <S.Span role="separator" />
-              </S.NavbarBurger>
+              <S.NavbarBrand>
+                <S.NavbarBurger
+                  onClick={onBurgerClick}
+                  onKeyDown={onBurgerClick}
+                  role="button"
+                  tabIndex={0}
+                  aria-label="burger"
+                >
+                  <S.Span role="separator" />
+                  <S.Span role="separator" />
+                  <S.Span role="separator" />
+                </S.NavbarBurger>
 
-              <S.Hyperlink to="/">
-                <Logo />
-                UI for Apache Kafka
-              </S.Hyperlink>
+                <S.Hyperlink to="/">
+                  <Logo />
+                  UI for Apache Kafka
+                </S.Hyperlink>
 
-              <S.NavbarItem>
-                {GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
-              </S.NavbarItem>
+                <S.NavbarItem>
+                  {GIT_TAG && <Version tag={GIT_TAG} commit={GIT_COMMIT} />}
+                </S.NavbarItem>
+              </S.NavbarBrand>
             </S.NavbarBrand>
-          </S.NavbarBrand>
-          <S.NavbarSocial>
-            <S.LogoutLink href="/logout">
-              <S.LogoutButton buttonType="primary" buttonSize="M">
-                Log out
-              </S.LogoutButton>
-            </S.LogoutLink>
-            <S.SocialLink
-              href="https://github.com/provectus/kafka-ui"
-              target="_blank"
-            >
-              <GitIcon />
-            </S.SocialLink>
-            <S.SocialLink
-              href="https://discord.com/invite/4DWzD7pGE5"
-              target="_blank"
-            >
-              <DiscordIcon />
-            </S.SocialLink>
-          </S.NavbarSocial>
-        </S.Navbar>
+            <S.NavbarSocial>
+              <S.LogoutLink href="/logout">
+                <S.LogoutButton buttonType="primary" buttonSize="M">
+                  Log out
+                </S.LogoutButton>
+              </S.LogoutLink>
+              <S.SocialLink
+                href="https://github.com/provectus/kafka-ui"
+                target="_blank"
+              >
+                <GitIcon />
+              </S.SocialLink>
+              <S.SocialLink
+                href="https://discord.com/invite/4DWzD7pGE5"
+                target="_blank"
+              >
+                <DiscordIcon />
+              </S.SocialLink>
+            </S.NavbarSocial>
+          </S.Navbar>
 
-        <S.Container>
-          <S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
-            <Suspense fallback={<PageLoader />}>
-              <Nav />
-            </Suspense>
-          </S.Sidebar>
-          <S.Overlay
-            $visible={isSidebarVisible}
-            onClick={closeSidebar}
-            onKeyDown={closeSidebar}
-            tabIndex={-1}
-            aria-hidden="true"
-            aria-label="Overlay"
-          />
-          <Routes>
-            {['/', '/ui', '/ui/clusters'].map((path) => (
+          <S.Container>
+            <S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
+              <Suspense fallback={<PageLoader />}>
+                <Nav />
+              </Suspense>
+            </S.Sidebar>
+            <S.Overlay
+              $visible={isSidebarVisible}
+              onClick={closeSidebar}
+              onKeyDown={closeSidebar}
+              tabIndex={-1}
+              aria-hidden="true"
+              aria-label="Overlay"
+            />
+            <Routes>
+              {['/', '/ui', '/ui/clusters'].map((path) => (
+                <Route
+                  key="Home" // optional: avoid full re-renders on route changes
+                  path={path}
+                  element={<Dashboard />}
+                />
+              ))}
               <Route
-                key="Home" // optional: avoid full re-renders on route changes
-                path={path}
-                element={<Dashboard />}
+                path={getNonExactPath(clusterPath())}
+                element={<ClusterPage />}
               />
-            ))}
-            <Route
-              path={getNonExactPath(clusterPath())}
-              element={<ClusterPage />}
-            />
-          </Routes>
-        </S.Container>
-        <S.AlertsContainer role="toolbar">
-          <Alerts />
-        </S.AlertsContainer>
-      </S.Layout>
-    </ThemeProvider>
+            </Routes>
+          </S.Container>
+          <S.AlertsContainer role="toolbar">
+            <Alerts />
+          </S.AlertsContainer>
+        </S.Layout>
+      </ThemeProvider>
+    </QueryClientProvider>
   );
 };
 

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

@@ -9,8 +9,8 @@ import {
   ClusterBrokerParam,
   clusterBrokerPath,
 } from 'lib/paths';
-import useClusterStats from 'lib/hooks/useClusterStats';
-import useBrokers from 'lib/hooks/useBrokers';
+import { useClusterStats } from 'lib/hooks/api/clusters';
+import { useBrokers } from 'lib/hooks/api/brokers';
 import { NavLink, Route, Routes } from 'react-router-dom';
 import BrokerLogdir from 'components/Brokers/Broker/BrokerLogdir/BrokerLogdir';
 import BrokerMetrics from 'components/Brokers/Broker/BrokerMetrics/BrokerMetrics';

+ 2 - 2
kafka-ui-react-app/src/components/Brokers/Broker/BrokerLogdir/BrokerLogdir.tsx

@@ -5,7 +5,7 @@ import { SmartTable } from 'components/common/SmartTable/SmartTable';
 import { TableColumn } from 'components/common/SmartTable/TableColumn';
 import { useTableState } from 'lib/hooks/useTableState';
 import { ClusterBrokerParam } from 'lib/paths';
-import useBrokersLogDirs from 'lib/hooks/useBrokersLogDirs';
+import { useBrokerLogDirs } from 'lib/hooks/api/brokers';
 
 export interface BrokerLogdirState {
   name: string;
@@ -17,7 +17,7 @@ export interface BrokerLogdirState {
 const BrokerLogdir: React.FC = () => {
   const { clusterName, brokerId } = useAppParams<ClusterBrokerParam>();
 
-  const { data: logDirs } = useBrokersLogDirs(clusterName, Number(brokerId));
+  const { data: logDirs } = useBrokerLogDirs(clusterName, Number(brokerId));
 
   const preparedRows = translateLogdirs(logDirs);
   const tableState = useTableState<BrokerLogdirState, string>(preparedRows, {

+ 2 - 2
kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/BrokerMetrics.tsx

@@ -1,14 +1,14 @@
 import React from 'react';
 import useAppParams from 'lib/hooks/useAppParams';
 import { ClusterBrokerParam } from 'lib/paths';
-import useBrokersMetrics from 'lib/hooks/useBrokersMetrics';
+import { useBrokerMetrics } from 'lib/hooks/api/brokers';
 import { SchemaType } from 'generated-sources';
 import EditorViewer from 'components/common/EditorViewer/EditorViewer';
 import { getEditorText } from 'components/Brokers/utils/getEditorText';
 
 const BrokerMetrics: React.FC = () => {
   const { clusterName, brokerId } = useAppParams<ClusterBrokerParam>();
-  const { data: metrics } = useBrokersMetrics(clusterName, Number(brokerId));
+  const { data: metrics } = useBrokerMetrics(clusterName, Number(brokerId));
 
   return (
     <EditorViewer schemaType={SchemaType.JSON} data={getEditorText(metrics)} />

+ 18 - 24
kafka-ui-react-app/src/components/Brokers/Broker/BrokerMetrics/__test__/BrokerMetrics.spec.tsx

@@ -1,37 +1,31 @@
 import React from 'react';
 import { render, WithRoute } from 'lib/testHelpers';
-import { screen, waitFor } from '@testing-library/dom';
+import { screen } from '@testing-library/dom';
 import { clusterBrokerMetricsPath } from 'lib/paths';
-import fetchMock from 'fetch-mock';
-import { act } from '@testing-library/react';
 import BrokerMetrics from 'components/Brokers/Broker/BrokerMetrics/BrokerMetrics';
+import { useBrokerMetrics } from 'lib/hooks/api/brokers';
+
+jest.mock('lib/hooks/api/brokers', () => ({
+  useBrokerMetrics: jest.fn(),
+}));
 
 const clusterName = 'local';
 const brokerId = 1;
-const fetchMetricsUrl = `/api/clusters/${clusterName}/brokers/${brokerId}/metrics`;
 
 describe('BrokerMetrics Component', () => {
-  afterEach(() => {
-    fetchMock.reset();
-  });
-
-  const renderComponent = async () => {
-    const fetchMetricsMock = fetchMock.getOnce(fetchMetricsUrl, {});
-    await act(() => {
-      render(
-        <WithRoute path={clusterBrokerMetricsPath()}>
-          <BrokerMetrics />
-        </WithRoute>,
-        {
-          initialEntries: [clusterBrokerMetricsPath(clusterName, brokerId)],
-        }
-      );
-    });
-    await waitFor(() => expect(fetchMetricsMock.called()).toBeTruthy());
-  };
-
   it("shows warning when server doesn't return metrics response", async () => {
-    await renderComponent();
+    (useBrokerMetrics as jest.Mock).mockImplementation(() => ({
+      data: {},
+    }));
+
+    render(
+      <WithRoute path={clusterBrokerMetricsPath()}>
+        <BrokerMetrics />
+      </WithRoute>,
+      {
+        initialEntries: [clusterBrokerMetricsPath(clusterName, brokerId)],
+      }
+    );
     expect(screen.getAllByRole('textbox').length).toEqual(1);
   });
 });

+ 25 - 26
kafka-ui-react-app/src/components/Brokers/Broker/__test__/Broker.spec.tsx

@@ -1,24 +1,22 @@
 import React from 'react';
 import { render, WithRoute } from 'lib/testHelpers';
-import { screen, waitFor } from '@testing-library/dom';
+import { screen } from '@testing-library/dom';
 import {
   clusterBrokerMetricsPath,
   clusterBrokerPath,
   getNonExactPath,
 } from 'lib/paths';
-import fetchMock from 'fetch-mock';
-import { act } from '@testing-library/react';
 import Broker from 'components/Brokers/Broker/Broker';
 import {
   clusterStatsPayload,
   brokersPayload,
 } from 'components/Brokers/__test__/fixtures';
+import { useBrokers } from 'lib/hooks/api/brokers';
+import { useClusterStats } from 'lib/hooks/api/clusters';
 
 const clusterName = 'local';
 const brokerId = 1;
 const activeClassName = 'is-active';
-const fetchStatsUrl = `/api/clusters/${clusterName}/stats`;
-const fetchBrokersUrl = `/api/clusters/${clusterName}/brokers`;
 const brokerLogdir = {
   pageText: 'brokerLogdir',
   navigationName: 'Log directories',
@@ -34,30 +32,31 @@ jest.mock('components/Brokers/Broker/BrokerLogdir/BrokerLogdir', () => () => (
 jest.mock('components/Brokers/Broker/BrokerMetrics/BrokerMetrics', () => () => (
   <div>{brokerMetrics.pageText}</div>
 ));
+jest.mock('lib/hooks/api/brokers', () => ({
+  useBrokers: jest.fn(),
+}));
+jest.mock('lib/hooks/api/clusters', () => ({
+  useClusterStats: jest.fn(),
+}));
 
 describe('Broker Component', () => {
-  afterEach(() => {
-    fetchMock.reset();
+  beforeEach(() => {
+    (useBrokers as jest.Mock).mockImplementation(() => ({
+      data: brokersPayload,
+    }));
+    (useClusterStats as jest.Mock).mockImplementation(() => ({
+      data: clusterStatsPayload,
+    }));
   });
-
-  const renderComponent = async (
-    path = clusterBrokerPath(clusterName, brokerId)
-  ) => {
-    const fetchStatsMock = fetchMock.get(fetchStatsUrl, clusterStatsPayload);
-    const fetchBrokersMock = fetchMock.get(fetchBrokersUrl, brokersPayload);
-    await act(() => {
-      render(
-        <WithRoute path={getNonExactPath(clusterBrokerPath())}>
-          <Broker />
-        </WithRoute>,
-        {
-          initialEntries: [path],
-        }
-      );
-    });
-    await waitFor(() => expect(fetchStatsMock.called()).toBeTruthy());
-    expect(fetchBrokersMock.called()).toBeTruthy();
-  };
+  const renderComponent = (path = clusterBrokerPath(clusterName, brokerId)) =>
+    render(
+      <WithRoute path={getNonExactPath(clusterBrokerPath())}>
+        <Broker />
+      </WithRoute>,
+      {
+        initialEntries: [path],
+      }
+    );
 
   it('shows broker found', async () => {
     await renderComponent();

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

@@ -7,8 +7,8 @@ import { Table } from 'components/common/table/Table/Table.styled';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import * as Metrics from 'components/common/Metrics';
 import useAppParams from 'lib/hooks/useAppParams';
-import useBrokers from 'lib/hooks/useBrokers';
-import useClusterStats from 'lib/hooks/useClusterStats';
+import { useBrokers } from 'lib/hooks/api/brokers';
+import { useClusterStats } from 'lib/hooks/api/clusters';
 
 import { ClickableRow } from './BrokersList.style';
 

+ 49 - 75
kafka-ui-react-app/src/components/Brokers/BrokersList/__test__/BrokersList.spec.tsx

@@ -10,6 +10,8 @@ import {
   clusterStatsPayload,
 } from 'components/Brokers/__test__/fixtures';
 import userEvent from '@testing-library/user-event';
+import { useBrokers } from 'lib/hooks/api/brokers';
+import { useClusterStats } from 'lib/hooks/api/clusters';
 
 const mockedUsedNavigate = jest.fn();
 
@@ -18,6 +20,13 @@ jest.mock('react-router-dom', () => ({
   useNavigate: () => mockedUsedNavigate,
 }));
 
+jest.mock('lib/hooks/api/brokers', () => ({
+  useBrokers: jest.fn(),
+}));
+jest.mock('lib/hooks/api/clusters', () => ({
+  useClusterStats: jest.fn(),
+}));
+
 describe('BrokersList Component', () => {
   afterEach(() => fetchMock.reset());
 
@@ -37,60 +46,36 @@ describe('BrokersList Component', () => {
     );
 
   describe('BrokersList', () => {
-    let fetchBrokersMock: fetchMock.FetchMockStatic;
-    const fetchStatsUrl = `/api/clusters/${clusterName}/stats`;
-
     beforeEach(() => {
-      fetchBrokersMock = fetchMock.get(
-        `/api/clusters/${clusterName}/brokers`,
-        brokersPayload
-      );
+      (useBrokers as jest.Mock).mockImplementation(() => ({
+        data: brokersPayload,
+      }));
+      (useClusterStats as jest.Mock).mockImplementation(() => ({
+        data: clusterStatsPayload,
+      }));
     });
 
     it('renders', async () => {
-      const fetchStatsMock = fetchMock.get(fetchStatsUrl, clusterStatsPayload);
-      await act(() => {
-        renderComponent();
-      });
-
-      await waitFor(() => expect(fetchStatsMock.called()).toBeTruthy());
-      await waitFor(() => expect(fetchBrokersMock.called()).toBeTruthy());
-
+      renderComponent();
       expect(screen.getByRole('table')).toBeInTheDocument();
       const rows = screen.getAllByRole('row');
       expect(rows.length).toEqual(3);
     });
-
     it('opens broker when row clicked', async () => {
-      const fetchStatsMock = fetchMock.get(fetchStatsUrl, clusterStatsPayload);
-      await act(() => {
-        renderComponent();
-      });
-      await waitFor(() => expect(fetchStatsMock.called()).toBeTruthy());
+      renderComponent();
       await act(() => {
         userEvent.click(screen.getByRole('cell', { name: '0' }));
       });
-
-      await waitFor(() => {
-        expect(mockedUsedNavigate).toBeCalled();
-        expect(mockedUsedNavigate).toBeCalledWith('0');
-      });
+      await waitFor(() => expect(mockedUsedNavigate).toBeCalledWith('0'));
     });
-
     it('shows warning when offlinePartitionCount > 0', async () => {
-      const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, {
-        ...clusterStatsPayload,
-        offlinePartitionCount: 1345,
-      });
-      await act(() => {
-        renderComponent();
-      });
-      await waitFor(() => {
-        expect(fetchStatsMock.called()).toBeTruthy();
-      });
-      await waitFor(() => {
-        expect(fetchBrokersMock.called()).toBeTruthy();
-      });
+      (useClusterStats as jest.Mock).mockImplementation(() => ({
+        data: {
+          ...clusterStatsPayload,
+          offlinePartitionCount: 1345,
+        },
+      }));
+      renderComponent();
       const onlineWidget = screen.getByText(
         clusterStatsPayload.onlinePartitionCount
       );
@@ -98,18 +83,14 @@ describe('BrokersList Component', () => {
       expect(onlineWidget).toHaveStyle({ color: '#E51A1A' });
     });
     it('shows right count when offlinePartitionCount > 0', async () => {
-      const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, {
-        ...clusterStatsPayload,
-        inSyncReplicasCount: testInSyncReplicasCount,
-        outOfSyncReplicasCount: testOutOfSyncReplicasCount,
-      });
-      await act(() => {
-        renderComponent();
-      });
-      await waitFor(() => {
-        expect(fetchStatsMock.called()).toBeTruthy();
-      });
-
+      (useClusterStats as jest.Mock).mockImplementation(() => ({
+        data: {
+          ...clusterStatsPayload,
+          inSyncReplicasCount: testInSyncReplicasCount,
+          outOfSyncReplicasCount: testOutOfSyncReplicasCount,
+        },
+      }));
+      renderComponent();
       const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);
       const onlineWidget = screen.getByText(
         `of ${testInSyncReplicasCount + testOutOfSyncReplicasCount}`
@@ -119,33 +100,26 @@ describe('BrokersList Component', () => {
     });
 
     it('shows right count when inSyncReplicasCount: undefined outOfSyncReplicasCount: 1', async () => {
-      const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, {
-        ...clusterStatsPayload,
-        inSyncReplicasCount: undefined,
-        outOfSyncReplicasCount: testOutOfSyncReplicasCount,
-      });
-      await act(() => {
-        renderComponent();
-      });
-      await waitFor(() => {
-        expect(fetchStatsMock.called()).toBeTruthy();
-      });
-
+      (useClusterStats as jest.Mock).mockImplementation(() => ({
+        data: {
+          ...clusterStatsPayload,
+          inSyncReplicasCount: undefined,
+          outOfSyncReplicasCount: testOutOfSyncReplicasCount,
+        },
+      }));
+      renderComponent();
       const onlineWidget = screen.getByText(`of ${testOutOfSyncReplicasCount}`);
       expect(onlineWidget).toBeInTheDocument();
     });
     it(`shows right count when inSyncReplicasCount: ${testInSyncReplicasCount} outOfSyncReplicasCount: undefined`, async () => {
-      const fetchStatsMock = fetchMock.getOnce(fetchStatsUrl, {
-        ...clusterStatsPayload,
-        inSyncReplicasCount: testInSyncReplicasCount,
-        outOfSyncReplicasCount: undefined,
-      });
-      await act(() => {
-        renderComponent();
-      });
-      await waitFor(() => {
-        expect(fetchStatsMock.called()).toBeTruthy();
-      });
+      (useClusterStats as jest.Mock).mockImplementation(() => ({
+        data: {
+          ...clusterStatsPayload,
+          inSyncReplicasCount: testInSyncReplicasCount,
+          outOfSyncReplicasCount: undefined,
+        },
+      }));
+      renderComponent();
       const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);
       const onlineWidget = screen.getByText(`of ${testInSyncReplicasCount}`);
       expect(onlineWidgetDef).toBeInTheDocument();

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

@@ -18,7 +18,7 @@ import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider';
 import PageLoader from 'components/common/PageLoader/PageLoader';
-import useClusters from 'lib/hooks/api/useClusters';
+import { useClusters } from 'lib/hooks/api/clusters';
 
 const Brokers = React.lazy(() => import('components/Brokers/Brokers'));
 const Topics = React.lazy(() => import('components/Topics/Topics'));

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

@@ -12,10 +12,10 @@ import {
 import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 import useAppParams from 'lib/hooks/useAppParams';
 
-import ListContainer from './List/ListContainer';
-import NewContainer from './New/NewContainer';
-import DetailsContainer from './Details/DetailsContainer';
-import EditContainer from './Edit/EditContainer';
+import ListPage from './List/ListPage';
+import New from './New/New';
+import Edit from './Edit/Edit';
+import DetailsPage from './Details/DetailsPage';
 
 const Connect: React.FC = () => {
   const { clusterName } = useAppParams();
@@ -26,7 +26,7 @@ const Connect: React.FC = () => {
         index
         element={
           <BreadcrumbRoute>
-            <ListContainer />
+            <ListPage />
           </BreadcrumbRoute>
         }
       />
@@ -34,7 +34,7 @@ const Connect: React.FC = () => {
         path={clusterConnectorNewRelativePath}
         element={
           <BreadcrumbRoute>
-            <NewContainer />
+            <New />
           </BreadcrumbRoute>
         }
       />
@@ -42,7 +42,7 @@ const Connect: React.FC = () => {
         path={clusterConnectConnectorEditRelativePath}
         element={
           <BreadcrumbRoute>
-            <EditContainer />
+            <Edit />
           </BreadcrumbRoute>
         }
       />
@@ -50,7 +50,7 @@ const Connect: React.FC = () => {
         path={getNonExactPath(clusterConnectConnectorRelativePath)}
         element={
           <BreadcrumbRoute>
-            <DetailsContainer />
+            <DetailsPage />
           </BreadcrumbRoute>
         }
       />

+ 54 - 95
kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx

@@ -1,15 +1,21 @@
 import React from 'react';
+import styled from 'styled-components';
 import { useNavigate } from 'react-router-dom';
-import useAppParams from 'lib/hooks/useAppParams';
+import { useIsMutating } from 'react-query';
 import { ConnectorState, ConnectorAction } from 'generated-sources';
-import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
+import useAppParams from 'lib/hooks/useAppParams';
+import useModal from 'lib/hooks/useModal';
+import {
+  useConnector,
+  useDeleteConnector,
+  useUpdateConnectorState,
+} from 'lib/hooks/api/kafkaConnect';
 import {
   clusterConnectConnectorEditPath,
   clusterConnectorsPath,
   RouterParamsClusterConnectConnector,
 } from 'lib/paths';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
-import styled from 'styled-components';
 import { Button } from 'components/common/Button/Button';
 
 const ConnectorActionsWrapperStyled = styled.div`
@@ -19,97 +25,51 @@ const ConnectorActionsWrapperStyled = styled.div`
   gap: 8px;
 `;
 
-export interface ActionsProps {
-  deleteConnector(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): Promise<unknown>;
-  isConnectorDeleting: boolean;
-  connectorStatus?: ConnectorState;
-  restartConnector(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): void;
-  restartTasks(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-    action: ConnectorAction;
-  }): void;
-  pauseConnector(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): void;
-  resumeConnector(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): void;
-  isConnectorActionRunning: boolean;
-}
-
-const Actions: React.FC<ActionsProps> = ({
-  deleteConnector,
-  isConnectorDeleting,
-  connectorStatus,
-  restartConnector,
-  restartTasks,
-  pauseConnector,
-  resumeConnector,
-  isConnectorActionRunning,
-}) => {
-  const { clusterName, connectName, connectorName } =
-    useAppParams<RouterParamsClusterConnectConnector>();
-
+const Actions: React.FC = () => {
   const navigate = useNavigate();
+  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
+  const mutationsNumber = useIsMutating();
+  const isMutating = mutationsNumber > 0;
+
+  const { data: connector } = useConnector(routerProps);
 
-  const [
-    isDeleteConnectorConfirmationVisible,
-    setIsDeleteConnectorConfirmationVisible,
-  ] = React.useState(false);
+  const {
+    isOpen: isDeleteConnectorConfirmationOpen,
+    setClose: setDeleteConnectorConfirmationClose,
+    setOpen: setDeleteConnectorConfirmationOpen,
+  } = useModal();
 
+  const deleteConnectorMutation = useDeleteConnector(routerProps);
   const deleteConnectorHandler = async () => {
     try {
-      await deleteConnector({ clusterName, connectName, connectorName });
-      navigate(clusterConnectorsPath(clusterName));
+      await deleteConnectorMutation.mutateAsync();
+      navigate(clusterConnectorsPath(routerProps.clusterName));
     } catch {
       // do not redirect
     }
   };
 
-  const restartConnectorHandler = () => {
-    restartConnector({ clusterName, connectName, connectorName });
-  };
-
-  const restartTasksHandler = (actionType: ConnectorAction) => {
-    restartTasks({
-      clusterName,
-      connectName,
-      connectorName,
-      action: actionType,
-    });
-  };
-
-  const pauseConnectorHandler = () => {
-    pauseConnector({ clusterName, connectName, connectorName });
-  };
-
-  const resumeConnectorHandler = () => {
-    resumeConnector({ clusterName, connectName, connectorName });
-  };
+  const stateMutation = useUpdateConnectorState(routerProps);
+  const restartConnectorHandler = () =>
+    stateMutation.mutateAsync(ConnectorAction.RESTART);
+  const restartAllTasksHandler = () =>
+    stateMutation.mutateAsync(ConnectorAction.RESTART_ALL_TASKS);
+  const restartFailedTasksHandler = () =>
+    stateMutation.mutateAsync(ConnectorAction.RESTART_FAILED_TASKS);
+  const pauseConnectorHandler = () =>
+    stateMutation.mutateAsync(ConnectorAction.PAUSE);
+  const resumeConnectorHandler = () =>
+    stateMutation.mutateAsync(ConnectorAction.RESUME);
 
   return (
     <ConnectorActionsWrapperStyled>
-      {connectorStatus === ConnectorState.RUNNING && (
+      {connector?.status.state === ConnectorState.RUNNING && (
         <Button
           buttonSize="M"
           buttonType="primary"
           type="button"
           onClick={pauseConnectorHandler}
-          disabled={isConnectorActionRunning}
+          disabled={isMutating}
         >
           <span>
             <i className="fas fa-pause" />
@@ -118,13 +78,13 @@ const Actions: React.FC<ActionsProps> = ({
         </Button>
       )}
 
-      {connectorStatus === ConnectorState.PAUSED && (
+      {connector?.status.state === ConnectorState.PAUSED && (
         <Button
           buttonSize="M"
           buttonType="primary"
           type="button"
           onClick={resumeConnectorHandler}
-          disabled={isConnectorActionRunning}
+          disabled={isMutating}
         >
           <span>
             <i className="fas fa-play" />
@@ -138,7 +98,7 @@ const Actions: React.FC<ActionsProps> = ({
         buttonType="primary"
         type="button"
         onClick={restartConnectorHandler}
-        disabled={isConnectorActionRunning}
+        disabled={isMutating}
       >
         <span>
           <i className="fas fa-sync-alt" />
@@ -149,8 +109,8 @@ const Actions: React.FC<ActionsProps> = ({
         buttonSize="M"
         buttonType="primary"
         type="button"
-        onClick={() => restartTasksHandler(ConnectorAction.RESTART_ALL_TASKS)}
-        disabled={isConnectorActionRunning}
+        onClick={restartAllTasksHandler}
+        disabled={isMutating}
       >
         <span>
           <i className="fas fa-sync-alt" />
@@ -161,10 +121,8 @@ const Actions: React.FC<ActionsProps> = ({
         buttonSize="M"
         buttonType="primary"
         type="button"
-        onClick={() =>
-          restartTasksHandler(ConnectorAction.RESTART_FAILED_TASKS)
-        }
-        disabled={isConnectorActionRunning}
+        onClick={restartFailedTasksHandler}
+        disabled={isMutating}
       >
         <span>
           <i className="fas fa-sync-alt" />
@@ -175,11 +133,11 @@ const Actions: React.FC<ActionsProps> = ({
         buttonSize="M"
         buttonType="primary"
         type="button"
-        disabled={isConnectorActionRunning}
+        disabled={isMutating}
         to={clusterConnectConnectorEditPath(
-          clusterName,
-          connectName,
-          connectorName
+          routerProps.clusterName,
+          routerProps.connectName,
+          routerProps.connectorName
         )}
       >
         <span>
@@ -192,8 +150,8 @@ const Actions: React.FC<ActionsProps> = ({
         buttonSize="M"
         buttonType="secondary"
         type="button"
-        onClick={() => setIsDeleteConnectorConfirmationVisible(true)}
-        disabled={isConnectorActionRunning}
+        onClick={setDeleteConnectorConfirmationOpen}
+        disabled={isMutating}
       >
         <span>
           <i className="far fa-trash-alt" />
@@ -201,12 +159,13 @@ const Actions: React.FC<ActionsProps> = ({
         <span>Delete</span>
       </Button>
       <ConfirmationModal
-        isOpen={isDeleteConnectorConfirmationVisible}
-        onCancel={() => setIsDeleteConnectorConfirmationVisible(false)}
+        isOpen={isDeleteConnectorConfirmationOpen}
+        onCancel={setDeleteConnectorConfirmationClose}
         onConfirm={deleteConnectorHandler}
-        isConfirming={isConnectorDeleting}
+        isConfirming={isMutating}
       >
-        Are you sure you want to remove <b>{connectorName}</b> connector?
+        Are you sure you want to remove <b>{routerProps.connectorName}</b>{' '}
+        connector?
       </ConfirmationModal>
     </ConnectorActionsWrapperStyled>
   );

+ 0 - 32
kafka-ui-react-app/src/components/Connect/Details/Actions/ActionsContainer.ts

@@ -1,32 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import {
-  deleteConnector,
-  restartConnector,
-  restartTasks,
-  pauseConnector,
-  resumeConnector,
-} from 'redux/reducers/connect/connectSlice';
-import {
-  getIsConnectorDeleting,
-  getConnectorStatus,
-  getIsConnectorActionRunning,
-} from 'redux/reducers/connect/selectors';
-
-import Actions from './Actions';
-
-const mapStateToProps = (state: RootState) => ({
-  isConnectorDeleting: getIsConnectorDeleting(state),
-  connectorStatus: getConnectorStatus(state),
-  isConnectorActionRunning: getIsConnectorActionRunning(state),
-});
-
-const mapDispatchToProps = {
-  deleteConnector,
-  restartConnector,
-  restartTasks,
-  pauseConnector,
-  resumeConnector,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Actions);

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

@@ -1,16 +1,16 @@
 import React from 'react';
 import { render, WithRoute } from 'lib/testHelpers';
-import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths';
-import ActionsContainer from 'components/Connect/Details/Actions/ActionsContainer';
-import Actions, {
-  ActionsProps,
-} from 'components/Connect/Details/Actions/Actions';
-import { ConnectorState } from 'generated-sources';
+import { clusterConnectConnectorPath } from 'lib/paths';
+import Actions from 'components/Connect/Details/Actions/Actions';
+import { ConnectorAction, ConnectorState } from 'generated-sources';
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import ConfirmationModal, {
-  ConfirmationModalProps,
-} from 'components/common/ConfirmationModal/ConfirmationModal';
+import {
+  useConnector,
+  useUpdateConnectorState,
+} from 'lib/hooks/api/kafkaConnect';
+import { connector } from 'lib/fixtures/kafkaConnect';
+import set from 'lodash/set';
 
 const mockHistoryPush = jest.fn();
 const deleteConnector = jest.fn();
@@ -21,6 +21,12 @@ jest.mock('react-router-dom', () => ({
   useNavigate: () => mockHistoryPush,
 }));
 
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnector: jest.fn(),
+  useDeleteConnector: jest.fn(),
+  useUpdateConnectorState: jest.fn(),
+}));
+
 jest.mock(
   'components/common/ConfirmationModal/ConfirmationModal',
   () => 'mock-ConfirmationModal'
@@ -41,107 +47,49 @@ describe('Actions', () => {
     cancelMock.mockClear();
   });
 
-  const actionsContainer = (props: Partial<ActionsProps> = {}) => (
-    <ActionsContainer>
-      <Actions
-        deleteConnector={jest.fn()}
-        isConnectorDeleting={false}
-        connectorStatus={ConnectorState.RUNNING}
-        restartConnector={jest.fn()}
-        restartTasks={jest.fn()}
-        pauseConnector={jest.fn()}
-        resumeConnector={jest.fn()}
-        isConnectorActionRunning={false}
-        {...props}
-      />
-    </ActionsContainer>
-  );
-
-  it('container renders view', () => {
-    const { container } = render(actionsContainer());
-    expect(container).toBeInTheDocument();
-  });
-
   describe('view', () => {
-    const pathname = clusterConnectConnectorPath();
-    const clusterName = 'my-cluster';
-    const connectName = 'my-connect';
-    const connectorName = 'my-connector';
-
-    const confirmationModal = (props: Partial<ConfirmationModalProps> = {}) => (
-      <WithRoute path={pathname}>
-        <ConfirmationModal
-          onCancel={cancelMock}
-          onConfirm={() =>
-            deleteConnector(clusterName, connectName, connectorName)
-          }
-          {...props}
-        >
-          <button type="button" onClick={cancelMock}>
-            Cancel
-          </button>
-          <button
-            type="button"
-            onClick={() => {
-              deleteConnector(clusterName, connectName, connectorName);
-              mockHistoryPush(clusterConnectorsPath(clusterName));
-            }}
-          >
-            Confirm
-          </button>
-        </ConfirmationModal>
-      </WithRoute>
+    const route = clusterConnectConnectorPath();
+    const path = clusterConnectConnectorPath(
+      'myCluster',
+      'myConnect',
+      'myConnector'
     );
 
-    const component = (props: Partial<ActionsProps> = {}) => (
-      <WithRoute path={pathname}>
-        <Actions
-          deleteConnector={jest.fn()}
-          isConnectorDeleting={false}
-          connectorStatus={ConnectorState.RUNNING}
-          restartConnector={jest.fn()}
-          restartTasks={jest.fn()}
-          pauseConnector={jest.fn()}
-          resumeConnector={jest.fn()}
-          isConnectorActionRunning={false}
-          {...props}
-        />
-      </WithRoute>
-    );
+    const renderComponent = () =>
+      render(
+        <WithRoute path={route}>
+          <Actions />
+        </WithRoute>,
+        { initialEntries: [path] }
+      );
 
     it('renders buttons when paused', () => {
-      render(component({ connectorStatus: ConnectorState.PAUSED }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
-      });
+      (useConnector as jest.Mock).mockImplementation(() => ({
+        data: set({ ...connector }, 'status.state', ConnectorState.PAUSED),
+      }));
+      renderComponent();
       expect(screen.getAllByRole('button').length).toEqual(6);
       expect(screen.getByText('Resume')).toBeInTheDocument();
       expect(screen.queryByText('Pause')).not.toBeInTheDocument();
-
       expectActionButtonsExists();
     });
 
     it('renders buttons when failed', () => {
-      render(component({ connectorStatus: ConnectorState.FAILED }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
-      });
+      (useConnector as jest.Mock).mockImplementation(() => ({
+        data: set({ ...connector }, 'status.state', ConnectorState.FAILED),
+      }));
+      renderComponent();
       expect(screen.getAllByRole('button').length).toEqual(5);
-
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
       expect(screen.queryByText('Pause')).not.toBeInTheDocument();
-
       expectActionButtonsExists();
     });
 
     it('renders buttons when unassigned', () => {
-      render(component({ connectorStatus: ConnectorState.UNASSIGNED }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
-      });
+      (useConnector as jest.Mock).mockImplementation(() => ({
+        data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED),
+      }));
+      renderComponent();
       expect(screen.getAllByRole('button').length).toEqual(5);
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
       expect(screen.queryByText('Pause')).not.toBeInTheDocument();
@@ -149,139 +97,92 @@ describe('Actions', () => {
     });
 
     it('renders buttons when running connector action', () => {
-      render(component({ connectorStatus: ConnectorState.RUNNING }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
-      });
+      (useConnector as jest.Mock).mockImplementation(() => ({
+        data: set({ ...connector }, 'status.state', ConnectorState.RUNNING),
+      }));
+      renderComponent();
       expect(screen.getAllByRole('button').length).toEqual(6);
       expect(screen.queryByText('Resume')).not.toBeInTheDocument();
       expect(screen.getByText('Pause')).toBeInTheDocument();
-
       expectActionButtonsExists();
     });
 
-    it('opens confirmation modal when delete button clicked', () => {
-      render(component({ deleteConnector }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
+    describe('mutations', () => {
+      beforeEach(() => {
+        (useConnector as jest.Mock).mockImplementation(() => ({
+          data: set({ ...connector }, 'status.state', ConnectorState.RUNNING),
+        }));
       });
-      userEvent.click(screen.getByRole('button', { name: 'Delete' }));
-
-      expect(
-        screen.getByText(/Are you sure you want to remove/i)
-      ).toHaveAttribute('isopen', 'true');
-    });
 
-    it('closes when cancel button clicked', () => {
-      render(confirmationModal({ isOpen: true }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
+      it('opens confirmation modal when delete button clicked', async () => {
+        renderComponent();
+        userEvent.click(screen.getByRole('button', { name: 'Delete' }));
+        expect(
+          screen.getByText(/Are you sure you want to remove/i)
+        ).toHaveAttribute('isopen', 'true');
       });
-      const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
-      userEvent.click(cancelBtn);
-      expect(cancelMock).toHaveBeenCalledTimes(1);
-    });
 
-    it('calls deleteConnector when confirm button clicked', () => {
-      render(confirmationModal({ isOpen: true }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
+      it('calls restartConnector when restart button clicked', () => {
+        const restartConnector = jest.fn();
+        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({
+          mutateAsync: restartConnector,
+        }));
+        renderComponent();
+        userEvent.click(
+          screen.getByRole('button', { name: 'Restart Connector' })
+        );
+        expect(restartConnector).toHaveBeenCalledWith(ConnectorAction.RESTART);
       });
-      const confirmBtn = screen.getByRole('button', { name: 'Confirm' });
-      userEvent.click(confirmBtn);
-      expect(deleteConnector).toHaveBeenCalledTimes(1);
-      expect(deleteConnector).toHaveBeenCalledWith(
-        clusterName,
-        connectName,
-        connectorName
-      );
-    });
 
-    it('redirects after delete', async () => {
-      render(confirmationModal({ isOpen: true }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
+      it('calls restartAllTasks', () => {
+        const restartAllTasks = jest.fn();
+        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({
+          mutateAsync: restartAllTasks,
+        }));
+        renderComponent();
+        userEvent.click(
+          screen.getByRole('button', { name: 'Restart All Tasks' })
+        );
+        expect(restartAllTasks).toHaveBeenCalledWith(
+          ConnectorAction.RESTART_ALL_TASKS
+        );
       });
-      const confirmBtn = screen.getByRole('button', { name: 'Confirm' });
-      userEvent.click(confirmBtn);
-      expect(mockHistoryPush).toHaveBeenCalledTimes(1);
-      expect(mockHistoryPush).toHaveBeenCalledWith(
-        clusterConnectorsPath(clusterName)
-      );
-    });
 
-    it('calls restartConnector when restart button clicked', () => {
-      const restartConnector = jest.fn();
-      render(component({ restartConnector }), {
-        initialEntries: [
-          clusterConnectConnectorPath(clusterName, connectName, connectorName),
-        ],
+      it('calls restartFailedTasks', () => {
+        const restartFailedTasks = jest.fn();
+        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({
+          mutateAsync: restartFailedTasks,
+        }));
+        renderComponent();
+        userEvent.click(
+          screen.getByRole('button', { name: 'Restart Failed Tasks' })
+        );
+        expect(restartFailedTasks).toHaveBeenCalledWith(
+          ConnectorAction.RESTART_FAILED_TASKS
+        );
       });
-      userEvent.click(
-        screen.getByRole('button', { name: 'Restart Connector' })
-      );
-      expect(restartConnector).toHaveBeenCalledTimes(1);
-      expect(restartConnector).toHaveBeenCalledWith({
-        clusterName,
-        connectName,
-        connectorName,
-      });
-    });
 
-    it('calls pauseConnector when pause button clicked', () => {
-      const pauseConnector = jest.fn();
-      render(
-        component({
-          connectorStatus: ConnectorState.RUNNING,
-          pauseConnector,
-        }),
-        {
-          initialEntries: [
-            clusterConnectConnectorPath(
-              clusterName,
-              connectName,
-              connectorName
-            ),
-          ],
-        }
-      );
-      userEvent.click(screen.getByRole('button', { name: 'Pause' }));
-      expect(pauseConnector).toHaveBeenCalledTimes(1);
-      expect(pauseConnector).toHaveBeenCalledWith({
-        clusterName,
-        connectName,
-        connectorName,
+      it('calls pauseConnector when pause button clicked', () => {
+        const pauseConnector = jest.fn();
+        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({
+          mutateAsync: pauseConnector,
+        }));
+        renderComponent();
+        userEvent.click(screen.getByRole('button', { name: 'Pause' }));
+        expect(pauseConnector).toHaveBeenCalledWith(ConnectorAction.PAUSE);
       });
-    });
 
-    it('calls resumeConnector when resume button clicked', () => {
-      const resumeConnector = jest.fn();
-      render(
-        component({
-          connectorStatus: ConnectorState.PAUSED,
-          resumeConnector,
-        }),
-        {
-          initialEntries: [
-            clusterConnectConnectorPath(
-              clusterName,
-              connectName,
-              connectorName
-            ),
-          ],
-        }
-      );
-      userEvent.click(screen.getByRole('button', { name: 'Resume' }));
-      expect(resumeConnector).toHaveBeenCalledTimes(1);
-      expect(resumeConnector).toHaveBeenCalledWith({
-        clusterName,
-        connectName,
-        connectorName,
+      it('calls resumeConnector when resume button clicked', () => {
+        const resumeConnector = jest.fn();
+        (useConnector as jest.Mock).mockImplementation(() => ({
+          data: set({ ...connector }, 'status.state', ConnectorState.PAUSED),
+        }));
+        (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({
+          mutateAsync: resumeConnector,
+        }));
+        renderComponent();
+        userEvent.click(screen.getByRole('button', { name: 'Resume' }));
+        expect(resumeConnector).toHaveBeenCalledWith(ConnectorAction.RESUME);
       });
     });
   });

+ 12 - 45
kafka-ui-react-app/src/components/Connect/Details/Config/Config.tsx

@@ -1,56 +1,23 @@
 import React from 'react';
 import useAppParams from 'lib/hooks/useAppParams';
-import {
-  ClusterName,
-  ConnectName,
-  ConnectorConfig,
-  ConnectorName,
-} from 'redux/interfaces';
-import PageLoader from 'components/common/PageLoader/PageLoader';
 import Editor from 'components/common/Editor/Editor';
-import styled from 'styled-components';
 import { RouterParamsClusterConnectConnector } from 'lib/paths';
+import { useConnectorConfig } from 'lib/hooks/api/kafkaConnect';
 
-export interface ConfigProps {
-  fetchConfig(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): void;
-  isConfigFetching: boolean;
-  config: ConnectorConfig | null;
-}
-
-const ConnectConfigWrapper = styled.div`
-  margin: 16px;
-`;
-
-const Config: React.FC<ConfigProps> = ({
-  fetchConfig,
-  isConfigFetching,
-  config,
-}) => {
-  const { clusterName, connectName, connectorName } =
-    useAppParams<RouterParamsClusterConnectConnector>();
-
-  React.useEffect(() => {
-    fetchConfig({ clusterName, connectName, connectorName });
-  }, [fetchConfig, clusterName, connectName, connectorName]);
-
-  if (isConfigFetching) {
-    return <PageLoader />;
-  }
+const Config: React.FC = () => {
+  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
+  const { data: config } = useConnectorConfig(routerProps);
 
   if (!config) return null;
+
   return (
-    <ConnectConfigWrapper>
-      <Editor
-        readOnly
-        value={JSON.stringify(config, null, '\t')}
-        highlightActiveLine={false}
-        isFixedHeight
-      />
-    </ConnectConfigWrapper>
+    <Editor
+      readOnly
+      value={JSON.stringify(config, null, '\t')}
+      highlightActiveLine={false}
+      isFixedHeight
+      style={{ margin: '16px' }}
+    />
   );
 };
 

+ 0 - 20
kafka-ui-react-app/src/components/Connect/Details/Config/ConfigContainer.ts

@@ -1,20 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import { fetchConnectorConfig } from 'redux/reducers/connect/connectSlice';
-import {
-  getIsConnectorConfigFetching,
-  getConnectorConfig,
-} from 'redux/reducers/connect/selectors';
-
-import Config from './Config';
-
-const mapStateToProps = (state: RootState) => ({
-  isConfigFetching: getIsConnectorConfigFetching(state),
-  config: getConnectorConfig(state),
-});
-
-const mapDispatchToProps = {
-  fetchConfig: fetchConnectorConfig,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Config);

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

@@ -1,71 +1,46 @@
 import React from 'react';
 import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConnectConnectorConfigPath } from 'lib/paths';
-import Config, { ConfigProps } from 'components/Connect/Details/Config/Config';
-import { connector } from 'redux/reducers/connect/__test__/fixtures';
+import Config from 'components/Connect/Details/Config/Config';
 import { screen } from '@testing-library/dom';
+import { useConnectorConfig } from 'lib/hooks/api/kafkaConnect';
+import { connector } from 'lib/fixtures/kafkaConnect';
 
-jest.mock('components/common/Editor/Editor', () => 'mock-Editor');
+jest.mock('components/common/Editor/Editor', () => () => (
+  <div>mock-Editor</div>
+));
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnectorConfig: jest.fn(),
+}));
 
 describe('Config', () => {
-  const pathname = clusterConnectConnectorConfigPath();
-  const clusterName = 'my-cluster';
-  const connectName = 'my-connect';
-  const connectorName = 'my-connector';
-
-  const component = (props: Partial<ConfigProps> = {}) => (
-    <WithRoute path={pathname}>
-      <Config
-        fetchConfig={jest.fn()}
-        isConfigFetching={false}
-        config={connector.config}
-        {...props}
-      />
-    </WithRoute>
-  );
-
-  it('to be in the document when fetching config', () => {
-    render(component({ isConfigFetching: true }), {
-      initialEntries: [
-        clusterConnectConnectorConfigPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
-      ],
-    });
-    expect(screen.getByRole('progressbar')).toBeInTheDocument();
-  });
+  const renderComponent = () =>
+    render(
+      <WithRoute path={clusterConnectConnectorConfigPath()}>
+        <Config />
+      </WithRoute>,
+      {
+        initialEntries: [
+          clusterConnectConnectorConfigPath(
+            'my-cluster',
+            'my-connect',
+            'my-connector'
+          ),
+        ],
+      }
+    );
 
   it('is empty when no config', () => {
-    const { container } = render(component({ config: null }), {
-      initialEntries: [
-        clusterConnectConnectorConfigPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
-      ],
-    });
+    (useConnectorConfig as jest.Mock).mockImplementation(() => ({}));
+    const { container } = renderComponent();
     expect(container).toBeEmptyDOMElement();
   });
 
-  it('fetches config on mount', () => {
-    const fetchConfig = jest.fn();
-    render(component({ fetchConfig }), {
-      initialEntries: [
-        clusterConnectConnectorConfigPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
-      ],
-    });
-    expect(fetchConfig).toHaveBeenCalledTimes(1);
-    expect(fetchConfig).toHaveBeenCalledWith({
-      clusterName,
-      connectName,
-      connectorName,
-    });
+  it('renders editor', () => {
+    (useConnectorConfig as jest.Mock).mockImplementation(() => ({
+      data: connector.config,
+    }));
+    renderComponent();
+    expect(screen.getByText('mock-Editor')).toBeInTheDocument();
   });
 });

+ 0 - 117
kafka-ui-react-app/src/components/Connect/Details/Details.tsx

@@ -1,117 +0,0 @@
-import React from 'react';
-import { NavLink, Route, Routes } from 'react-router-dom';
-import useAppParams from 'lib/hooks/useAppParams';
-import { Connector, Task } from 'generated-sources';
-import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
-import {
-  clusterConnectConnectorConfigPath,
-  clusterConnectConnectorConfigRelativePath,
-  clusterConnectConnectorPath,
-  clusterConnectConnectorTasksPath,
-  clusterConnectConnectorTasksRelativePath,
-  RouterParamsClusterConnectConnector,
-} from 'lib/paths';
-import PageLoader from 'components/common/PageLoader/PageLoader';
-import Navbar from 'components/common/Navigation/Navbar.styled';
-import PageHeading from 'components/common/PageHeading/PageHeading';
-
-import OverviewContainer from './Overview/OverviewContainer';
-import TasksContainer from './Tasks/TasksContainer';
-import ConfigContainer from './Config/ConfigContainer';
-import ActionsContainer from './Actions/ActionsContainer';
-
-export interface DetailsProps {
-  fetchConnector(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): void;
-  fetchTasks(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): void;
-  isConnectorFetching: boolean;
-  areTasksFetching: boolean;
-  connector: Connector | null;
-  tasks: Task[];
-}
-
-const Details: React.FC<DetailsProps> = ({
-  fetchConnector,
-  fetchTasks,
-  isConnectorFetching,
-  areTasksFetching,
-  connector,
-}) => {
-  const { clusterName, connectName, connectorName } =
-    useAppParams<RouterParamsClusterConnectConnector>();
-
-  React.useEffect(() => {
-    fetchConnector({ clusterName, connectName, connectorName });
-  }, [fetchConnector, clusterName, connectName, connectorName]);
-
-  React.useEffect(() => {
-    fetchTasks({ clusterName, connectName, connectorName });
-  }, [fetchTasks, clusterName, connectName, connectorName]);
-
-  if (isConnectorFetching || areTasksFetching) {
-    return <PageLoader />;
-  }
-
-  if (!connector) return null;
-
-  return (
-    <div>
-      <PageHeading text={connectorName}>
-        <ActionsContainer />
-      </PageHeading>
-      <Navbar role="navigation">
-        <NavLink
-          to={clusterConnectConnectorPath(
-            clusterName,
-            connectName,
-            connectorName
-          )}
-          className={({ isActive }) => (isActive ? 'is-active' : '')}
-          end
-        >
-          Overview
-        </NavLink>
-        <NavLink
-          to={clusterConnectConnectorTasksPath(
-            clusterName,
-            connectName,
-            connectorName
-          )}
-          className={({ isActive }) => (isActive ? 'is-active' : '')}
-        >
-          Tasks
-        </NavLink>
-        <NavLink
-          to={clusterConnectConnectorConfigPath(
-            clusterName,
-            connectName,
-            connectorName
-          )}
-          className={({ isActive }) => (isActive ? 'is-active' : '')}
-        >
-          Config
-        </NavLink>
-      </Navbar>
-      <Routes>
-        <Route index element={<OverviewContainer />} />
-        <Route
-          path={clusterConnectConnectorTasksRelativePath}
-          element={<TasksContainer />}
-        />
-        <Route
-          path={clusterConnectConnectorConfigRelativePath}
-          element={<ConfigContainer />}
-        />
-      </Routes>
-    </div>
-  );
-};
-
-export default Details;

+ 0 - 28
kafka-ui-react-app/src/components/Connect/Details/DetailsContainer.ts

@@ -1,28 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import {
-  fetchConnector,
-  fetchConnectorTasks,
-} from 'redux/reducers/connect/connectSlice';
-import {
-  getIsConnectorFetching,
-  getAreConnectorTasksFetching,
-  getConnector,
-  getConnectorTasks,
-} from 'redux/reducers/connect/selectors';
-
-import Details from './Details';
-
-const mapStateToProps = (state: RootState) => ({
-  isConnectorFetching: getIsConnectorFetching(state),
-  connector: getConnector(state),
-  areTasksFetching: getAreConnectorTasksFetching(state),
-  tasks: getConnectorTasks(state),
-});
-
-const mapDispatchToProps = {
-  fetchConnector,
-  fetchTasks: fetchConnectorTasks,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Details);

+ 80 - 0
kafka-ui-react-app/src/components/Connect/Details/DetailsPage.tsx

@@ -0,0 +1,80 @@
+import React, { Suspense } from 'react';
+import { NavLink, Route, Routes } from 'react-router-dom';
+import useAppParams from 'lib/hooks/useAppParams';
+import {
+  clusterConnectConnectorConfigPath,
+  clusterConnectConnectorConfigRelativePath,
+  clusterConnectConnectorPath,
+  clusterConnectConnectorTasksPath,
+  clusterConnectConnectorTasksRelativePath,
+  RouterParamsClusterConnectConnector,
+} from 'lib/paths';
+import Navbar from 'components/common/Navigation/Navbar.styled';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+
+import Overview from './Overview/Overview';
+import Tasks from './Tasks/Tasks';
+import Config from './Config/Config';
+import Actions from './Actions/Actions';
+
+const DetailsPage: React.FC = () => {
+  const { clusterName, connectName, connectorName } =
+    useAppParams<RouterParamsClusterConnectConnector>();
+
+  return (
+    <div>
+      <PageHeading text={connectorName}>
+        <Actions />
+      </PageHeading>
+      <Navbar role="navigation">
+        <NavLink
+          to={clusterConnectConnectorPath(
+            clusterName,
+            connectName,
+            connectorName
+          )}
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
+          end
+        >
+          Overview
+        </NavLink>
+        <NavLink
+          to={clusterConnectConnectorTasksPath(
+            clusterName,
+            connectName,
+            connectorName
+          )}
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
+        >
+          Tasks
+        </NavLink>
+        <NavLink
+          to={clusterConnectConnectorConfigPath(
+            clusterName,
+            connectName,
+            connectorName
+          )}
+          className={({ isActive }) => (isActive ? 'is-active' : '')}
+        >
+          Config
+        </NavLink>
+      </Navbar>
+      <Suspense fallback={<PageLoader />}>
+        <Routes>
+          <Route index element={<Overview />} />
+          <Route
+            path={clusterConnectConnectorTasksRelativePath}
+            element={<Tasks />}
+          />
+          <Route
+            path={clusterConnectConnectorConfigRelativePath}
+            element={<Config />}
+          />
+        </Routes>
+      </Suspense>
+    </div>
+  );
+};
+
+export default DetailsPage;

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

@@ -1,21 +1,24 @@
 import React from 'react';
-import { Connector } from 'generated-sources';
 import * as C from 'components/common/Tag/Tag.styled';
 import * as Metrics from 'components/common/Metrics';
 import getTagColor from 'components/common/Tag/getTagColor';
+import { RouterParamsClusterConnectConnector } from 'lib/paths';
+import useAppParams from 'lib/hooks/useAppParams';
+import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
 
-export interface OverviewProps {
-  connector: Connector | null;
-  runningTasksCount: number;
-  failedTasksCount: number;
-}
+import getTaskMetrics from './getTaskMetrics';
 
-const Overview: React.FC<OverviewProps> = ({
-  connector,
-  runningTasksCount,
-  failedTasksCount,
-}) => {
-  if (!connector) return null;
+const Overview: React.FC = () => {
+  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
+
+  const { data: connector } = useConnector(routerProps);
+  const { data: tasks } = useConnectorTasks(routerProps);
+
+  if (!connector) {
+    return null;
+  }
+
+  const { running, failed } = getTaskMetrics(tasks);
 
   return (
     <Metrics.Wrapper>
@@ -36,15 +39,13 @@ const Overview: React.FC<OverviewProps> = ({
             {connector.status.state}
           </C.Tag>
         </Metrics.Indicator>
-        <Metrics.Indicator label="Tasks Running">
-          {runningTasksCount}
-        </Metrics.Indicator>
+        <Metrics.Indicator label="Tasks Running">{running}</Metrics.Indicator>
         <Metrics.Indicator
           label="Tasks Failed"
           isAlert
-          alertType={failedTasksCount > 0 ? 'error' : 'success'}
+          alertType={failed > 0 ? 'error' : 'success'}
         >
-          {failedTasksCount}
+          {failed}
         </Metrics.Indicator>
       </Metrics.Section>
     </Metrics.Wrapper>

+ 0 - 17
kafka-ui-react-app/src/components/Connect/Details/Overview/OverviewContainer.ts

@@ -1,17 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import {
-  getConnector,
-  getConnectorRunningTasksCount,
-  getConnectorFailedTasksCount,
-} from 'redux/reducers/connect/selectors';
-
-import Overview from './Overview';
-
-const mapStateToProps = (state: RootState) => ({
-  connector: getConnector(state),
-  runningTasksCount: getConnectorRunningTasksCount(state),
-  failedTasksCount: getConnectorFailedTasksCount(state),
-});
-
-export default connect(mapStateToProps)(Overview);

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

@@ -1,40 +1,57 @@
 import React from 'react';
 import Overview from 'components/Connect/Details/Overview/Overview';
-import { connector } from 'redux/reducers/connect/__test__/fixtures';
+import { connector, tasks } from 'lib/fixtures/kafkaConnect';
 import { screen } from '@testing-library/react';
 import { render } from 'lib/testHelpers';
+import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
+
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnector: jest.fn(),
+  useConnectorTasks: jest.fn(),
+}));
 
 describe('Overview', () => {
   it('is empty when no connector', () => {
-    const { container } = render(
-      <Overview connector={null} runningTasksCount={10} failedTasksCount={2} />
-    );
+    (useConnector as jest.Mock).mockImplementation(() => ({
+      data: undefined,
+    }));
+    (useConnectorTasks as jest.Mock).mockImplementation(() => ({
+      data: undefined,
+    }));
+
+    const { container } = render(<Overview />);
     expect(container).toBeEmptyDOMElement();
   });
 
-  it('renders metrics', () => {
-    const running = 234789237;
-    const failed = 373737;
-    render(
-      <Overview
-        connector={connector}
-        runningTasksCount={running}
-        failedTasksCount={failed}
-      />
-    );
-    expect(screen.getByText('Worker')).toBeInTheDocument();
-    expect(
-      screen.getByText(connector.status.workerId as string)
-    ).toBeInTheDocument();
-
-    expect(screen.getByText('Type')).toBeInTheDocument();
-    expect(
-      screen.getByText(connector.config['connector.class'] as string)
-    ).toBeInTheDocument();
-
-    expect(screen.getByText('Tasks Running')).toBeInTheDocument();
-    expect(screen.getByText(running)).toBeInTheDocument();
-    expect(screen.getByText('Tasks Failed')).toBeInTheDocument();
-    expect(screen.getByText(failed)).toBeInTheDocument();
+  describe('when connector is loaded', () => {
+    beforeEach(() => {
+      (useConnector as jest.Mock).mockImplementation(() => ({
+        data: connector,
+      }));
+    });
+    beforeEach(() => {
+      (useConnectorTasks as jest.Mock).mockImplementation(() => ({
+        data: tasks,
+      }));
+    });
+
+    it('renders metrics', () => {
+      render(<Overview />);
+
+      expect(screen.getByText('Worker')).toBeInTheDocument();
+      expect(
+        screen.getByText(connector.status.workerId as string)
+      ).toBeInTheDocument();
+
+      expect(screen.getByText('Type')).toBeInTheDocument();
+      expect(
+        screen.getByText(connector.config['connector.class'] as string)
+      ).toBeInTheDocument();
+
+      expect(screen.getByText('Tasks Running')).toBeInTheDocument();
+      expect(screen.getByText(2)).toBeInTheDocument();
+      expect(screen.getByText('Tasks Failed')).toBeInTheDocument();
+      expect(screen.getByText(1)).toBeInTheDocument();
+    });
   });
 });

+ 19 - 0
kafka-ui-react-app/src/components/Connect/Details/Overview/__tests__/getTaskMetrics.spec.ts

@@ -0,0 +1,19 @@
+import { tasks } from 'lib/fixtures/kafkaConnect';
+import getTaskMetrics from 'components/Connect/Details/Overview/getTaskMetrics';
+
+describe('getTaskMetrics', () => {
+  it('should return the correct metrics when task list is undefined', () => {
+    const metrics = getTaskMetrics();
+    expect(metrics).toEqual({
+      running: 0,
+      failed: 0,
+    });
+  });
+
+  it('should return the correct metrics', () => {
+    expect(getTaskMetrics(tasks)).toEqual({
+      running: 2,
+      failed: 1,
+    });
+  });
+});

+ 23 - 0
kafka-ui-react-app/src/components/Connect/Details/Overview/getTaskMetrics.ts

@@ -0,0 +1,23 @@
+import { ConnectorTaskStatus, Task } from 'generated-sources';
+
+export default function getTaskMetrics(tasks?: Task[]) {
+  const initialMetrics = {
+    running: 0,
+    failed: 0,
+  };
+
+  if (!tasks) {
+    return initialMetrics;
+  }
+
+  return tasks.reduce((acc, { status }) => {
+    const state = status?.state;
+    if (state === ConnectorTaskStatus.RUNNING) {
+      return { ...acc, running: acc.running + 1 };
+    }
+    if (state === ConnectorTaskStatus.FAILED) {
+      return { ...acc, failed: acc.failed + 1 };
+    }
+    return acc;
+  }, initialMetrics);
+}

+ 0 - 56
kafka-ui-react-app/src/components/Connect/Details/Tasks/ListItem/ListItem.tsx

@@ -1,56 +0,0 @@
-import React from 'react';
-import useAppParams from 'lib/hooks/useAppParams';
-import { Task, TaskId } from 'generated-sources';
-import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
-import Dropdown from 'components/common/Dropdown/Dropdown';
-import DropdownItem from 'components/common/Dropdown/DropdownItem';
-import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
-import * as C from 'components/common/Tag/Tag.styled';
-import getTagColor from 'components/common/Tag/getTagColor';
-import { RouterParamsClusterConnectConnector } from 'lib/paths';
-
-export interface ListItemProps {
-  task: Task;
-  restartTask(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-    taskId: TaskId['task'];
-  }): Promise<unknown>;
-}
-
-const ListItem: React.FC<ListItemProps> = ({ task, restartTask }) => {
-  const { clusterName, connectName, connectorName } =
-    useAppParams<RouterParamsClusterConnectConnector>();
-
-  const restartTaskHandler = async () => {
-    await restartTask({
-      clusterName,
-      connectName,
-      connectorName,
-      taskId: task.id?.task,
-    });
-  };
-
-  return (
-    <tr>
-      <td>{task.status?.id}</td>
-      <td>{task.status?.workerId}</td>
-      <td>
-        <C.Tag color={getTagColor(task.status)}>{task.status.state}</C.Tag>
-      </td>
-      <td>{task.status.trace || 'null'}</td>
-      <td style={{ width: '5%' }}>
-        <div>
-          <Dropdown label={<VerticalElipsisIcon />} right>
-            <DropdownItem onClick={restartTaskHandler} danger>
-              <span>Restart task</span>
-            </DropdownItem>
-          </Dropdown>
-        </div>
-      </td>
-    </tr>
-  );
-};
-
-export default ListItem;

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

@@ -1,20 +0,0 @@
-import { connect } from 'react-redux';
-import { Task } from 'generated-sources';
-import { RootState } from 'redux/interfaces';
-import { restartConnectorTask } from 'redux/reducers/connect/connectSlice';
-
-import ListItem from './ListItem';
-
-interface OwnProps {
-  task: Task;
-}
-
-const mapStateToProps = (_state: RootState, { task }: OwnProps) => ({
-  task,
-});
-
-const mapDispatchToProps = {
-  restartTask: restartConnectorTask,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(ListItem);

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

@@ -1,70 +0,0 @@
-import React from 'react';
-import { render, WithRoute } from 'lib/testHelpers';
-import { clusterConnectConnectorTasksPath } from 'lib/paths';
-import ListItem, {
-  ListItemProps,
-} from 'components/Connect/Details/Tasks/ListItem/ListItem';
-import { tasks } from 'redux/reducers/connect/__test__/fixtures';
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-const pathname = clusterConnectConnectorTasksPath();
-const clusterName = 'my-cluster';
-const connectName = 'my-connect';
-const connectorName = 'my-connector';
-const restartTask = jest.fn();
-const task = tasks[0];
-
-const renderComponent = (props: ListItemProps = { task, restartTask }) => {
-  return render(
-    <WithRoute path={pathname}>
-      <table>
-        <tbody>
-          <ListItem {...props} />
-        </tbody>
-      </table>
-    </WithRoute>,
-    {
-      initialEntries: [
-        clusterConnectConnectorTasksPath(
-          clusterName,
-          connectName,
-          connectorName
-        ),
-      ],
-    }
-  );
-};
-
-describe('ListItem', () => {
-  it('renders', () => {
-    renderComponent();
-    expect(screen.getByRole('row')).toBeInTheDocument();
-    expect(
-      screen.getByRole('cell', { name: task.status.id.toString() })
-    ).toBeInTheDocument();
-    expect(
-      screen.getByRole('cell', { name: task.status.workerId })
-    ).toBeInTheDocument();
-    expect(
-      screen.getByRole('cell', { name: task.status.state })
-    ).toBeInTheDocument();
-    expect(screen.getByRole('button')).toBeInTheDocument();
-    expect(screen.getByRole('menu')).toBeInTheDocument();
-    expect(screen.getByRole('menuitem')).toBeInTheDocument();
-  });
-  it('calls restartTask on button click', () => {
-    renderComponent();
-
-    expect(restartTask).not.toBeCalled();
-    userEvent.click(screen.getByRole('button'));
-    userEvent.click(screen.getByRole('menuitem'));
-    expect(restartTask).toBeCalledTimes(1);
-    expect(restartTask).toHaveBeenCalledWith({
-      clusterName,
-      connectName,
-      connectorName,
-      taskId: task.id?.task,
-    });
-  });
-});

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

@@ -1,20 +1,27 @@
 import React from 'react';
-import { Task } from 'generated-sources';
-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 {
+  useConnectorTasks,
+  useRestartConnectorTask,
+} from 'lib/hooks/api/kafkaConnect';
+import useAppParams from 'lib/hooks/useAppParams';
+import { RouterParamsClusterConnectConnector } from 'lib/paths';
+import Dropdown from 'components/common/Dropdown/Dropdown';
+import DropdownItem from 'components/common/Dropdown/DropdownItem';
+import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
+import getTagColor from 'components/common/Tag/getTagColor';
+import { Tag } from 'components/common/Tag/Tag.styled';
 
-import ListItemContainer from './ListItem/ListItemContainer';
+const Tasks: React.FC = () => {
+  const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
+  const { data: tasks } = useConnectorTasks(routerProps);
+  const restartMutation = useRestartConnectorTask(routerProps);
 
-export interface TasksProps {
-  areTasksFetching: boolean;
-  tasks: Task[];
-}
-
-const Tasks: React.FC<TasksProps> = ({ areTasksFetching, tasks }) => {
-  if (areTasksFetching) {
-    return <PageLoader />;
-  }
+  const restartTaskHandler = (taskId?: number) => {
+    if (taskId === undefined) return;
+    restartMutation.mutateAsync(taskId);
+  };
 
   return (
     <Table isFullwidth>
@@ -28,13 +35,32 @@ const Tasks: React.FC<TasksProps> = ({ areTasksFetching, tasks }) => {
         </tr>
       </thead>
       <tbody>
-        {tasks.length === 0 && (
+        {tasks?.length === 0 && (
           <tr>
             <td colSpan={10}>No tasks found</td>
           </tr>
         )}
-        {tasks.map((task) => (
-          <ListItemContainer key={task.status?.id} task={task} />
+        {tasks?.map((task) => (
+          <tr key={task.status?.id}>
+            <td>{task.status?.id}</td>
+            <td>{task.status?.workerId}</td>
+            <td>
+              <Tag color={getTagColor(task.status)}>{task.status.state}</Tag>
+            </td>
+            <td>{task.status.trace || 'null'}</td>
+            <td style={{ width: '5%' }}>
+              <div>
+                <Dropdown label={<VerticalElipsisIcon />} right>
+                  <DropdownItem
+                    onClick={() => restartTaskHandler(task.id?.task)}
+                    danger
+                  >
+                    <span>Restart task</span>
+                  </DropdownItem>
+                </Dropdown>
+              </div>
+            </td>
+          </tr>
         ))}
       </tbody>
     </Table>

+ 0 - 20
kafka-ui-react-app/src/components/Connect/Details/Tasks/TasksContainer.ts

@@ -1,20 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import { fetchConnectorTasks } from 'redux/reducers/connect/connectSlice';
-import {
-  getAreConnectorTasksFetching,
-  getConnectorTasks,
-} from 'redux/reducers/connect/selectors';
-
-import Tasks from './Tasks';
-
-const mapStateToProps = (state: RootState) => ({
-  areTasksFetching: getAreConnectorTasksFetching(state),
-  tasks: getConnectorTasks(state),
-});
-
-const mapDispatchToProps = {
-  fetchTasks: fetchConnectorTasks,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Tasks);

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

@@ -1,59 +1,42 @@
 import React from 'react';
 import { render, WithRoute } from 'lib/testHelpers';
 import { clusterConnectConnectorTasksPath } from 'lib/paths';
-import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer';
-import Tasks, { TasksProps } from 'components/Connect/Details/Tasks/Tasks';
-import { tasks } from 'redux/reducers/connect/__test__/fixtures';
+import Tasks from 'components/Connect/Details/Tasks/Tasks';
+import { tasks } from 'lib/fixtures/kafkaConnect';
 import { screen } from '@testing-library/dom';
+import { useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
 
-jest.mock(
-  'components/Connect/Details/Tasks/ListItem/ListItemContainer',
-  () => 'tr'
-);
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnectorTasks: jest.fn(),
+  useRestartConnectorTask: jest.fn(),
+}));
 
-describe('Tasks', () => {
-  it('container renders view', () => {
-    render(<TasksContainer />);
-    expect(screen.getByRole('table')).toBeInTheDocument();
-  });
-
-  describe('view', () => {
-    const clusterName = 'my-cluster';
-    const connectName = 'my-connect';
-    const connectorName = 'my-connector';
+const path = clusterConnectConnectorTasksPath('local', 'ghp', '1');
 
-    const setupWrapper = (props: Partial<TasksProps> = {}) => (
+describe('Tasks', () => {
+  const renderComponent = () =>
+    render(
       <WithRoute path={clusterConnectConnectorTasksPath()}>
-        <Tasks areTasksFetching={false} tasks={tasks} {...props} />
-      </WithRoute>
+        <Tasks />
+      </WithRoute>,
+      { initialEntries: [path] }
     );
 
-    it('to be in the document when fetching tasks', () => {
-      render(setupWrapper({ areTasksFetching: true }), {
-        initialEntries: [
-          clusterConnectConnectorTasksPath(
-            clusterName,
-            connectName,
-            connectorName
-          ),
-        ],
-      });
-      expect(screen.getByRole('progressbar')).toBeInTheDocument();
-      expect(screen.queryByRole('table')).not.toBeInTheDocument();
-    });
+  it('renders empty table', () => {
+    (useConnectorTasks as jest.Mock).mockImplementation(() => ({
+      data: [],
+    }));
+
+    renderComponent();
+    expect(screen.getByRole('table')).toBeInTheDocument();
+    expect(screen.getByText('No tasks found')).toBeInTheDocument();
+  });
 
-    it('to be in the document when no tasks', () => {
-      render(setupWrapper({ tasks: [] }), {
-        initialEntries: [
-          clusterConnectConnectorTasksPath(
-            clusterName,
-            connectName,
-            connectorName
-          ),
-        ],
-      });
-      expect(screen.getByRole('table')).toBeInTheDocument();
-      expect(screen.getByText('No tasks found')).toBeInTheDocument();
-    });
+  it('renders tasks table', () => {
+    (useConnectorTasks as jest.Mock).mockImplementation(() => ({
+      data: tasks,
+    }));
+    renderComponent();
+    expect(screen.getAllByRole('row').length).toEqual(tasks.length + 1);
   });
 });

+ 0 - 136
kafka-ui-react-app/src/components/Connect/Details/__tests__/Details.spec.tsx

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

+ 84 - 0
kafka-ui-react-app/src/components/Connect/Details/__tests__/DetailsPage.spec.tsx

@@ -0,0 +1,84 @@
+import React from 'react';
+import { render, WithRoute } from 'lib/testHelpers';
+import {
+  clusterConnectConnectorConfigPath,
+  clusterConnectConnectorPath,
+  clusterConnectConnectorTasksPath,
+  getNonExactPath,
+} from 'lib/paths';
+import { screen } from '@testing-library/dom';
+import DetailsPage from 'components/Connect/Details/DetailsPage';
+
+const DetailsCompText = {
+  overview: 'Overview Page',
+  tasks: 'Tasks Page',
+  config: 'Config Page',
+  actions: 'Actions',
+};
+
+jest.mock('components/Connect/Details/Overview/Overview', () => () => (
+  <div>{DetailsCompText.overview}</div>
+));
+
+jest.mock('components/Connect/Details/Tasks/Tasks', () => () => (
+  <div>{DetailsCompText.tasks}</div>
+));
+
+jest.mock('components/Connect/Details/Config/Config', () => () => (
+  <div>{DetailsCompText.config}</div>
+));
+
+jest.mock('components/Connect/Details/Actions/Actions', () => () => (
+  <div>{DetailsCompText.actions}</div>
+));
+
+describe('Details Page', () => {
+  const clusterName = 'my-cluster';
+  const connectName = 'my-connect';
+  const connectorName = 'my-connector';
+  const defaultPath = clusterConnectConnectorPath(
+    clusterName,
+    connectName,
+    connectorName
+  );
+
+  const renderComponent = (path: string = defaultPath) =>
+    render(
+      <WithRoute path={getNonExactPath(clusterConnectConnectorPath())}>
+        <DetailsPage />
+      </WithRoute>,
+      { initialEntries: [path] }
+    );
+
+  it('renders actions', () => {
+    renderComponent();
+    expect(screen.getByText(DetailsCompText.actions));
+  });
+
+  describe('Router component tests', () => {
+    it('should test if overview is rendering', () => {
+      renderComponent();
+      expect(screen.getByText(DetailsCompText.overview));
+    });
+
+    it('should test if tasks is rendering', () => {
+      const path = clusterConnectConnectorTasksPath(
+        clusterName,
+        connectName,
+        connectorName
+      );
+      renderComponent(path);
+      expect(screen.getByText(DetailsCompText.tasks));
+    });
+
+    it('should test if list is rendering', () => {
+      const path = clusterConnectConnectorConfigPath(
+        clusterName,
+        connectName,
+        connectorName
+      );
+      renderComponent(path);
+      expect(screen.getByText(DetailsCompText.config));
+    });
+  });
+});

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

@@ -4,20 +4,17 @@ import useAppParams from 'lib/hooks/useAppParams';
 import { Controller, useForm } from 'react-hook-form';
 import { ErrorMessage } from '@hookform/error-message';
 import { yupResolver } from '@hookform/resolvers/yup';
-import {
-  ClusterName,
-  ConnectName,
-  ConnectorConfig,
-  ConnectorName,
-} from 'redux/interfaces';
 import {
   clusterConnectConnectorConfigPath,
   RouterParamsClusterConnectConnector,
 } from 'lib/paths';
 import yup from 'lib/yupExtended';
 import Editor from 'components/common/Editor/Editor';
-import PageLoader from 'components/common/PageLoader/PageLoader';
 import { Button } from 'components/common/Button/Button';
+import {
+  useConnectorConfig,
+  useUpdateConnectorConfig,
+} from 'lib/hooks/api/kafkaConnect';
 
 import {
   ConnectEditWarningMessageStyled,
@@ -32,31 +29,12 @@ interface FormValues {
   config: string;
 }
 
-export interface EditProps {
-  fetchConfig(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }): Promise<unknown>;
-  isConfigFetching: boolean;
-  config: ConnectorConfig | null;
-  updateConfig(payload: {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-    connectorConfig: ConnectorConfig;
-  }): Promise<unknown>;
-}
-
-const Edit: React.FC<EditProps> = ({
-  fetchConfig,
-  isConfigFetching,
-  config,
-  updateConfig,
-}) => {
-  const { clusterName, connectName, connectorName } =
-    useAppParams<RouterParamsClusterConnectConnector>();
+const Edit: React.FC = () => {
+  const routerParams = useAppParams<RouterParamsClusterConnectConnector>();
   const navigate = useNavigate();
+  const { data: config } = useConnectorConfig(routerParams);
+  const mutation = useUpdateConnectorConfig(routerParams);
+
   const {
     handleSubmit,
     control,
@@ -70,10 +48,6 @@ const Edit: React.FC<EditProps> = ({
     },
   });
 
-  React.useEffect(() => {
-    fetchConfig({ clusterName, connectName, connectorName });
-  }, [fetchConfig, clusterName, connectName, connectorName]);
-
   React.useEffect(() => {
     if (config) {
       setValue('config', JSON.stringify(config, null, '\t'));
@@ -81,25 +55,20 @@ const Edit: React.FC<EditProps> = ({
   }, [config, setValue]);
 
   const onSubmit = async (values: FormValues) => {
-    const connector = await updateConfig({
-      clusterName,
-      connectName,
-      connectorName,
-      connectorConfig: JSON.parse(values.config.trim()),
-    });
+    const requestBody = JSON.parse(values.config.trim());
+    const connector = await mutation.mutateAsync(requestBody);
+
     if (connector) {
       navigate(
         clusterConnectConnectorConfigPath(
-          clusterName,
-          connectName,
-          connectorName
+          routerParams.clusterName,
+          routerParams.connectName,
+          routerParams.connectorName
         )
       );
     }
   };
 
-  if (isConfigFetching) return <PageLoader />;
-
   const hasCredentials = JSON.stringify(config, null, '\t').includes(
     '"******"'
   );

+ 0 - 24
kafka-ui-react-app/src/components/Connect/Edit/EditContainer.ts

@@ -1,24 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import {
-  fetchConnectorConfig,
-  updateConnectorConfig,
-} from 'redux/reducers/connect/connectSlice';
-import {
-  getConnectorConfig,
-  getIsConnectorConfigFetching,
-} from 'redux/reducers/connect/selectors';
-
-import Edit from './Edit';
-
-const mapStateToProps = (state: RootState) => ({
-  isConfigFetching: getIsConnectorConfigFetching(state),
-  config: getConnectorConfig(state),
-});
-
-const mapDispatchToProps = {
-  fetchConfig: fetchConnectorConfig,
-  updateConfig: updateConnectorConfig,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(Edit);

+ 37 - 41
kafka-ui-react-app/src/components/Connect/Edit/__tests__/Edit.spec.tsx

@@ -4,12 +4,14 @@ import {
   clusterConnectConnectorConfigPath,
   clusterConnectConnectorEditPath,
 } from 'lib/paths';
-import Edit, { EditProps } from 'components/Connect/Edit/Edit';
-import { connector } from 'redux/reducers/connect/__test__/fixtures';
+import Edit from 'components/Connect/Edit/Edit';
+import { connector } from 'lib/fixtures/kafkaConnect';
 import { waitFor } from '@testing-library/dom';
 import { act, fireEvent, screen } from '@testing-library/react';
-
-jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
+import {
+  useConnectorConfig,
+  useUpdateConnectorConfig,
+} from 'lib/hooks/api/kafkaConnect';
 
 jest.mock('components/common/Editor/Editor', () => 'mock-Editor');
 
@@ -18,23 +20,23 @@ jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
   useNavigate: () => mockHistoryPush,
 }));
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnectorConfig: jest.fn(),
+  useUpdateConnectorConfig: jest.fn(),
+}));
+
+const [clusterName, connectName, connectorName] = [
+  'my-cluster',
+  'my-connect',
+  'my-connector',
+];
 
 describe('Edit', () => {
   const pathname = clusterConnectConnectorEditPath();
-  const clusterName = 'my-cluster';
-  const connectName = 'my-connect';
-  const connectorName = 'my-connector';
-
-  const renderComponent = (props: Partial<EditProps> = {}) =>
+  const renderComponent = () =>
     render(
       <WithRoute path={pathname}>
-        <Edit
-          fetchConfig={jest.fn()}
-          isConfigFetching={false}
-          config={connector.config}
-          updateConfig={jest.fn()}
-          {...props}
-        />
+        <Edit />
       </WithRoute>,
       {
         initialEntries: [
@@ -47,34 +49,23 @@ describe('Edit', () => {
       }
     );
 
-  it('fetches config on mount', async () => {
-    const fetchConfig = jest.fn();
-    await waitFor(() => renderComponent({ fetchConfig }));
-    expect(fetchConfig).toHaveBeenCalledTimes(1);
-    expect(fetchConfig).toHaveBeenCalledWith({
-      clusterName,
-      connectName,
-      connectorName,
-    });
+  beforeEach(() => {
+    (useConnectorConfig as jest.Mock).mockImplementation(() => ({
+      data: connector.config,
+    }));
   });
 
-  it('calls updateConfig on form submit', async () => {
-    const updateConfig = jest.fn();
-    await waitFor(() => renderComponent({ updateConfig }));
-    fireEvent.submit(screen.getByRole('form'));
-    await waitFor(() => expect(updateConfig).toHaveBeenCalledTimes(1));
-    expect(updateConfig).toHaveBeenCalledWith({
-      clusterName,
-      connectName,
-      connectorName,
-      connectorConfig: connector.config,
+  it('calls updateConfig and redirects to connector config view on successful submit', async () => {
+    const updateConfig = jest.fn(() => {
+      return Promise.resolve(connector);
     });
-  });
+    (useUpdateConnectorConfig as jest.Mock).mockImplementation(() => ({
+      mutateAsync: updateConfig,
+    }));
 
-  it('redirects to connector config view on successful submit', async () => {
-    const updateConfig = jest.fn().mockResolvedValueOnce(connector);
-    await waitFor(() => renderComponent({ updateConfig }));
+    renderComponent();
     fireEvent.submit(screen.getByRole('form'));
+    await waitFor(() => expect(updateConfig).toHaveBeenCalledTimes(1));
 
     await waitFor(() => expect(mockHistoryPush).toHaveBeenCalledTimes(1));
     expect(mockHistoryPush).toHaveBeenCalledWith(
@@ -83,8 +74,13 @@ describe('Edit', () => {
   });
 
   it('does not redirect to connector config view on unsuccessful submit', async () => {
-    const updateConfig = jest.fn().mockResolvedValueOnce(undefined);
-    await waitFor(() => renderComponent({ updateConfig }));
+    const updateConfig = jest.fn(() => {
+      return Promise.resolve();
+    });
+    (useUpdateConnectorConfig as jest.Mock).mockImplementation(() => ({
+      mutateAsync: updateConfig,
+    }));
+    renderComponent();
     await act(() => {
       fireEvent.submit(screen.getByRole('form'));
     });

+ 33 - 126
kafka-ui-react-app/src/components/Connect/List/List.tsx

@@ -1,140 +1,47 @@
 import React from 'react';
 import useAppParams from 'lib/hooks/useAppParams';
-import { Connect, FullConnectorInfo } from 'generated-sources';
-import { ClusterName, ConnectorSearch } from 'redux/interfaces';
-import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths';
-import ClusterContext from 'components/contexts/ClusterContext';
-import PageLoader from 'components/common/PageLoader/PageLoader';
-import Search from 'components/common/Search/Search';
-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 { ClusterNameRoute } from 'lib/paths';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
+import useSearch from 'lib/hooks/useSearch';
+import { useConnectors } from 'lib/hooks/api/kafkaConnect';
 
 import ListItem from './ListItem';
 
-export interface ListProps {
-  areConnectsFetching: boolean;
-  areConnectorsFetching: boolean;
-  connectors: FullConnectorInfo[];
-  connects: Connect[];
-  failedConnectors: FullConnectorInfo[];
-  failedTasks: number | undefined;
-  fetchConnects(clusterName: ClusterName): void;
-  fetchConnectors({ clusterName }: { clusterName: ClusterName }): void;
-  search: string;
-  setConnectorSearch(value: ConnectorSearch): void;
-}
-
-const List: React.FC<ListProps> = ({
-  connectors,
-  areConnectsFetching,
-  areConnectorsFetching,
-  failedConnectors,
-  failedTasks,
-  fetchConnects,
-  fetchConnectors,
-  search,
-  setConnectorSearch,
-}) => {
-  const { isReadOnly } = React.useContext(ClusterContext);
+const List: React.FC = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();
-
-  React.useEffect(() => {
-    fetchConnects(clusterName);
-    fetchConnectors({ clusterName });
-  }, [fetchConnects, fetchConnectors, clusterName]);
-
-  const handleSearch = (value: string) =>
-    setConnectorSearch({
-      clusterName,
-      search: value,
-    });
+  const [search] = useSearch();
+  const { data: connectors } = useConnectors(clusterName, search);
 
   return (
-    <>
-      <PageHeading text="Connectors">
-        {!isReadOnly && (
-          <Button
-            buttonType="primary"
-            buttonSize="M"
-            to={clusterConnectorNewRelativePath}
-          >
-            Create Connector
-          </Button>
+    <Table isFullwidth>
+      <thead>
+        <tr>
+          <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>
+        {(!connectors || connectors.length) === 0 && (
+          <tr>
+            <td colSpan={10}>No connectors found</td>
+          </tr>
         )}
-      </PageHeading>
-      <Metrics.Wrapper>
-        <Metrics.Section>
-          <Metrics.Indicator
-            label="Connectors"
-            title="Connectors"
-            fetching={areConnectsFetching}
-          >
-            {connectors.length}
-          </Metrics.Indicator>
-          <Metrics.Indicator
-            label="Failed Connectors"
-            title="Failed Connectors"
-            fetching={areConnectsFetching}
-          >
-            {failedConnectors?.length}
-          </Metrics.Indicator>
-          <Metrics.Indicator
-            label="Failed Tasks"
-            title="Failed Tasks"
-            fetching={areConnectsFetching}
-          >
-            {failedTasks}
-          </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>
-          <Table isFullwidth>
-            <thead>
-              <tr>
-                <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>
-              {connectors.length === 0 && (
-                <tr>
-                  <td colSpan={10}>No connectors found</td>
-                </tr>
-              )}
-              {connectors.map((connector) => (
-                <ListItem
-                  key={[connector.name, connector.connect, clusterName].join(
-                    '-'
-                  )}
-                  connector={connector}
-                  clusterName={clusterName}
-                />
-              ))}
-            </tbody>
-          </Table>
-        </div>
-      )}
-    </>
+        {connectors?.map((connector) => (
+          <ListItem
+            key={connector.name}
+            connector={connector}
+            clusterName={clusterName}
+          />
+        ))}
+      </tbody>
+    </Table>
   );
 };
 

+ 0 - 37
kafka-ui-react-app/src/components/Connect/List/ListContainer.ts

@@ -1,37 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import {
-  fetchConnects,
-  fetchConnectors,
-  setConnectorSearch,
-} from 'redux/reducers/connect/connectSlice';
-import {
-  getConnects,
-  getConnectors,
-  getAreConnectsFetching,
-  getAreConnectorsFetching,
-  getConnectorSearch,
-  getFailedConnectors,
-  getSortedTopics,
-  getFailedTasks,
-} from 'redux/reducers/connect/selectors';
-import List from 'components/Connect/List/List';
-
-const mapStateToProps = (state: RootState) => ({
-  areConnectsFetching: getAreConnectsFetching(state),
-  areConnectorsFetching: getAreConnectorsFetching(state),
-  connects: getConnects(state),
-  failedConnectors: getFailedConnectors(state),
-  sortedTopics: getSortedTopics(state),
-  failedTasks: getFailedTasks(state),
-  connectors: getConnectors(state),
-  search: getConnectorSearch(state),
-});
-
-const mapDispatchToProps = {
-  fetchConnects,
-  fetchConnectors,
-  setConnectorSearch,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(List);

+ 14 - 24
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

@@ -3,8 +3,6 @@ import { FullConnectorInfo } from 'generated-sources';
 import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
 import { ClusterName } from 'redux/interfaces';
 import { Link, NavLink } from 'react-router-dom';
-import { useDispatch } from 'react-redux';
-import { deleteConnector } from 'redux/reducers/connect/connectSlice';
 import Dropdown from 'components/common/Dropdown/Dropdown';
 import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
@@ -12,6 +10,8 @@ import { Tag } from 'components/common/Tag/Tag.styled';
 import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
 import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import getTagColor from 'components/common/Tag/getTagColor';
+import useModal from 'lib/hooks/useModal';
+import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect';
 
 import * as S from './List.styled';
 
@@ -33,23 +33,16 @@ const ListItem: React.FC<ListItemProps> = ({
     failedTasksCount,
   },
 }) => {
-  const dispatch = useDispatch();
-  const [
-    isDeleteConnectorConfirmationVisible,
-    setDeleteConnectorConfirmationVisible,
-  ] = React.useState(false);
+  const { isOpen, setClose, setOpen } = useModal();
+  const deleteMutation = useDeleteConnector({
+    clusterName,
+    connectName: connect,
+    connectorName: name,
+  });
 
-  const handleDelete = () => {
-    if (clusterName && connect && name) {
-      dispatch(
-        deleteConnector({
-          clusterName,
-          connectName: connect,
-          connectorName: name,
-        })
-      );
-    }
-    setDeleteConnectorConfirmationVisible(false);
+  const handleDelete = async () => {
+    await deleteMutation.mutateAsync();
+    setClose();
   };
 
   const runningTasks = React.useMemo(() => {
@@ -87,17 +80,14 @@ const ListItem: React.FC<ListItemProps> = ({
       <td>
         <div>
           <Dropdown label={<VerticalElipsisIcon />} right up>
-            <DropdownItem
-              onClick={() => setDeleteConnectorConfirmationVisible(true)}
-              danger
-            >
+            <DropdownItem onClick={setOpen} danger>
               Remove Connector
             </DropdownItem>
           </Dropdown>
         </div>
         <ConfirmationModal
-          isOpen={isDeleteConnectorConfirmationVisible}
-          onCancel={() => setDeleteConnectorConfirmationVisible(false)}
+          isOpen={isOpen}
+          onCancel={setClose}
           onConfirm={handleDelete}
         >
           Are you sure want to remove <b>{name}</b> connector?

+ 86 - 0
kafka-ui-react-app/src/components/Connect/List/ListPage.tsx

@@ -0,0 +1,86 @@
+import React, { Suspense } from 'react';
+import useAppParams from 'lib/hooks/useAppParams';
+import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths';
+import ClusterContext from 'components/contexts/ClusterContext';
+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 useSearch from 'lib/hooks/useSearch';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+import { ConnectorState } from 'generated-sources';
+import { useConnectors } from 'lib/hooks/api/kafkaConnect';
+
+import List from './List';
+
+const ListPage: React.FC = () => {
+  const { isReadOnly } = React.useContext(ClusterContext);
+  const { clusterName } = useAppParams<ClusterNameRoute>();
+  const [search, handleSearch] = useSearch();
+
+  // Fetches all connectors from the API, without search criteria. Used to display general metrics.
+  const { data: connectorsMetrics, isLoading } = useConnectors(clusterName);
+
+  const numberOfFailedConnectors = connectorsMetrics?.filter(
+    ({ status: { state } }) => state === ConnectorState.FAILED
+  ).length;
+
+  const numberOfFailedTasks = connectorsMetrics?.reduce(
+    (acc, metric) => acc + (metric.failedTasksCount ?? 0),
+    0
+  );
+
+  return (
+    <>
+      <PageHeading text="Connectors">
+        {!isReadOnly && (
+          <Button
+            buttonType="primary"
+            buttonSize="M"
+            to={clusterConnectorNewRelativePath}
+          >
+            Create Connector
+          </Button>
+        )}
+      </PageHeading>
+      <Metrics.Wrapper>
+        <Metrics.Section>
+          <Metrics.Indicator
+            label="Connectors"
+            title="Total number of connectors"
+            fetching={isLoading}
+          >
+            {connectorsMetrics?.length || '-'}
+          </Metrics.Indicator>
+          <Metrics.Indicator
+            label="Failed Connectors"
+            title="Number of failed connectors"
+            fetching={isLoading}
+          >
+            {numberOfFailedConnectors ?? '-'}
+          </Metrics.Indicator>
+          <Metrics.Indicator
+            label="Failed Tasks"
+            title="Number of failed tasks"
+            fetching={isLoading}
+          >
+            {numberOfFailedTasks ?? '-'}
+          </Metrics.Indicator>
+        </Metrics.Section>
+      </Metrics.Wrapper>
+      <ControlPanelWrapper hasInput>
+        <Search
+          handleSearch={handleSearch}
+          placeholder="Search by Connect Name, Status or Type"
+          value={search}
+        />
+      </ControlPanelWrapper>
+      <Suspense fallback={<PageLoader />}>
+        <List />
+      </Suspense>
+    </>
+  );
+};
+
+export default ListPage;

+ 45 - 90
kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx

@@ -1,101 +1,56 @@
 import React from 'react';
-import {
-  connectors,
-  failedConnectors,
-} from 'redux/reducers/connect/__test__/fixtures';
+import { connectors } from 'lib/fixtures/kafkaConnect';
 import ClusterContext, {
   ContextProps,
   initialValue,
 } from 'components/contexts/ClusterContext';
-import ListContainer from 'components/Connect/List/ListContainer';
-import List, { ListProps } from 'components/Connect/List/List';
-import { act, screen } from '@testing-library/react';
-import { render } from 'lib/testHelpers';
+import List from 'components/Connect/List/List';
+import { screen } from '@testing-library/react';
+import { render, WithRoute } from 'lib/testHelpers';
+import fetchMock from 'fetch-mock';
+import { clusterConnectorsPath } from 'lib/paths';
+import { useConnectors } from 'lib/hooks/api/kafkaConnect';
+
+jest.mock('components/Connect/List/ListItem', () => () => (
+  <tr>
+    <td>List Item</td>
+  </tr>
+));
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnectors: jest.fn(),
+}));
+
+const clusterName = 'local';
 
 describe('Connectors List', () => {
-  describe('Container', () => {
-    it('renders view with initial state of storage', async () => {
-      await act(() => {
-        render(<ListContainer />);
-      });
-      expect(screen.getByRole('heading')).toHaveTextContent('Connectors');
-    });
+  afterEach(() => fetchMock.restore());
+
+  const renderComponent = (contextValue: ContextProps = initialValue) =>
+    render(
+      <ClusterContext.Provider value={contextValue}>
+        <WithRoute path={clusterConnectorsPath()}>
+          <List />
+        </WithRoute>
+      </ClusterContext.Provider>,
+      { initialEntries: [clusterConnectorsPath(clusterName)] }
+    );
+
+  it('renders empty connectors Table', async () => {
+    (useConnectors as jest.Mock).mockImplementation(() => ({
+      data: [],
+    }));
+
+    await renderComponent();
+    expect(screen.getByRole('table')).toBeInTheDocument();
+    expect(screen.getByText('No connectors found')).toBeInTheDocument();
   });
 
-  describe('View', () => {
-    const fetchConnects = jest.fn();
-    const fetchConnectors = jest.fn();
-    const setConnectorSearch = jest.fn();
-    const renderComponent = (
-      props: Partial<ListProps> = {},
-      contextValue: ContextProps = initialValue
-    ) => {
-      render(
-        <ClusterContext.Provider value={contextValue}>
-          <List
-            areConnectorsFetching
-            areConnectsFetching
-            connectors={[]}
-            failedConnectors={[]}
-            failedTasks={0}
-            connects={[]}
-            fetchConnects={fetchConnects}
-            fetchConnectors={fetchConnectors}
-            search=""
-            setConnectorSearch={setConnectorSearch}
-            {...props}
-          />
-        </ClusterContext.Provider>
-      );
-    };
-
-    it('renders PageLoader', async () => {
-      await act(() => renderComponent({ areConnectorsFetching: true }));
-      expect(screen.getByRole('progressbar')).toBeInTheDocument();
-      expect(screen.queryByRole('row')).not.toBeInTheDocument();
-    });
-
-    it('renders table', () => {
-      renderComponent({ areConnectorsFetching: false });
-      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
-      expect(screen.getByRole('table')).toBeInTheDocument();
-    });
-
-    it('renders connectors list', () => {
-      renderComponent({
-        areConnectorsFetching: false,
-        connectors,
-      });
-      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
-      expect(screen.getByRole('table')).toBeInTheDocument();
-      expect(screen.getAllByRole('row').length).toEqual(3);
-    });
-
-    it('renders failed connectors list', () => {
-      renderComponent({
-        areConnectorsFetching: false,
-        failedConnectors,
-      });
-      expect(screen.queryByRole('PageLoader')).not.toBeInTheDocument();
-      expect(screen.getByTitle('Failed Connectors')).toBeInTheDocument();
-    });
-
-    it('handles fetchConnects and fetchConnectors', () => {
-      renderComponent();
-      expect(fetchConnects).toHaveBeenCalledTimes(1);
-      expect(fetchConnectors).toHaveBeenCalledTimes(1);
-    });
-
-    it('renders actions if cluster is not readonly', () => {
-      renderComponent({}, { ...initialValue, isReadOnly: false });
-      expect(screen.getByRole('button')).toBeInTheDocument();
-    });
-
-    describe('readonly cluster', () => {
-      it('does not render actions if cluster is readonly', () => {
-        renderComponent({}, { ...initialValue, isReadOnly: true });
-        expect(screen.queryByRole('button')).not.toBeInTheDocument();
-      });
-    });
+  it('renders connectors Table', async () => {
+    (useConnectors as jest.Mock).mockImplementation(() => ({
+      data: connectors,
+    }));
+    await renderComponent();
+    expect(screen.getByRole('table')).toBeInTheDocument();
+    expect(screen.getAllByText('List Item').length).toEqual(2);
   });
 });

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

@@ -1,18 +1,11 @@
 import React from 'react';
-import { connectors } from 'redux/reducers/connect/__test__/fixtures';
+import { connectors } from 'lib/fixtures/kafkaConnect';
 import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 
-const mockDeleteConnector = jest.fn(() => ({ type: 'test' }));
-
-jest.mock('redux/reducers/connect/connectSlice', () => ({
-  ...jest.requireActual('redux/reducers/connect/connectSlice'),
-  deleteConnector: () => mockDeleteConnector,
-}));
-
 jest.mock(
   'components/common/ConfirmationModal/ConfirmationModal',
   () => 'mock-ConfirmationModal'

+ 182 - 0
kafka-ui-react-app/src/components/Connect/List/__tests__/ListPage.spec.tsx

@@ -0,0 +1,182 @@
+import React from 'react';
+import { connectors } from 'lib/fixtures/kafkaConnect';
+import ClusterContext, {
+  ContextProps,
+  initialValue,
+} from 'components/contexts/ClusterContext';
+import ListPage from 'components/Connect/List/ListPage';
+import { screen, within } from '@testing-library/react';
+import { render, WithRoute } from 'lib/testHelpers';
+import fetchMock from 'fetch-mock';
+import { clusterConnectorsPath } from 'lib/paths';
+import { useConnectors } from 'lib/hooks/api/kafkaConnect';
+
+jest.mock('components/Connect/List/List', () => () => (
+  <div>Connectors List</div>
+));
+
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnectors: jest.fn(),
+}));
+
+const clusterName = 'local';
+
+describe('Connectors List Page', () => {
+  afterEach(() => fetchMock.restore());
+
+  beforeEach(() => {
+    (useConnectors as jest.Mock).mockImplementation(() => ({
+      isLoading: false,
+      data: [],
+    }));
+  });
+
+  const renderComponent = async (contextValue: ContextProps = initialValue) =>
+    render(
+      <ClusterContext.Provider value={contextValue}>
+        <WithRoute path={clusterConnectorsPath()}>
+          <ListPage />
+        </WithRoute>
+      </ClusterContext.Provider>,
+      { initialEntries: [clusterConnectorsPath(clusterName)] }
+    );
+
+  describe('Heading', () => {
+    it('renders header without create button for readonly cluster', async () => {
+      await renderComponent({ ...initialValue, isReadOnly: true });
+      expect(
+        screen.getByRole('heading', { name: 'Connectors' })
+      ).toBeInTheDocument();
+      expect(
+        screen.queryByRole('link', { name: 'Create Connector' })
+      ).not.toBeInTheDocument();
+    });
+
+    it('renders header with create button for read/write cluster', async () => {
+      await renderComponent();
+      expect(
+        screen.getByRole('heading', { name: 'Connectors' })
+      ).toBeInTheDocument();
+      expect(
+        screen.getByRole('link', { name: 'Create Connector' })
+      ).toBeInTheDocument();
+    });
+  });
+
+  it('renders search input', async () => {
+    await renderComponent();
+    expect(
+      screen.getByPlaceholderText('Search by Connect Name, Status or Type')
+    ).toBeInTheDocument();
+  });
+
+  it('renders list', async () => {
+    await renderComponent();
+    expect(screen.getByText('Connectors List')).toBeInTheDocument();
+  });
+
+  describe('Metrics', () => {
+    it('renders indicators in loading state', async () => {
+      (useConnectors as jest.Mock).mockImplementation(() => ({
+        isLoading: true,
+        data: connectors,
+      }));
+
+      await renderComponent();
+      const metrics = screen.getByRole('group');
+      expect(metrics).toBeInTheDocument();
+      expect(within(metrics).getAllByRole('progressbar').length).toEqual(3);
+    });
+
+    it('renders indicators for empty list of connectors', async () => {
+      await renderComponent();
+      const metrics = screen.getByRole('group');
+      expect(metrics).toBeInTheDocument();
+
+      const connectorsIndicator = within(metrics).getByTitle(
+        'Total number of connectors'
+      );
+      expect(connectorsIndicator).toBeInTheDocument();
+      expect(connectorsIndicator).toHaveTextContent('Connectors -');
+
+      const failedConnectorsIndicator = within(metrics).getByTitle(
+        'Number of failed connectors'
+      );
+      expect(failedConnectorsIndicator).toBeInTheDocument();
+      expect(failedConnectorsIndicator).toHaveTextContent(
+        'Failed Connectors 0'
+      );
+
+      const failedTasksIndicator = within(metrics).getByTitle(
+        'Number of failed tasks'
+      );
+      expect(failedTasksIndicator).toBeInTheDocument();
+      expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 0');
+    });
+
+    it('renders indicators when connectors list is undefined', async () => {
+      (useConnectors as jest.Mock).mockImplementation(() => ({
+        isFetching: false,
+        data: undefined,
+      }));
+
+      await renderComponent();
+      const metrics = screen.getByRole('group');
+      expect(metrics).toBeInTheDocument();
+
+      const connectorsIndicator = within(metrics).getByTitle(
+        'Total number of connectors'
+      );
+      expect(connectorsIndicator).toBeInTheDocument();
+      expect(connectorsIndicator).toHaveTextContent('Connectors -');
+
+      const failedConnectorsIndicator = within(metrics).getByTitle(
+        'Number of failed connectors'
+      );
+      expect(failedConnectorsIndicator).toBeInTheDocument();
+      expect(failedConnectorsIndicator).toHaveTextContent(
+        'Failed Connectors -'
+      );
+
+      const failedTasksIndicator = within(metrics).getByTitle(
+        'Number of failed tasks'
+      );
+      expect(failedTasksIndicator).toBeInTheDocument();
+      expect(failedTasksIndicator).toHaveTextContent('Failed Tasks -');
+    });
+
+    it('renders indicators list of connectors', async () => {
+      (useConnectors as jest.Mock).mockImplementation(() => ({
+        isLoading: false,
+        data: connectors,
+      }));
+
+      await renderComponent();
+
+      const metrics = screen.getByRole('group');
+      expect(metrics).toBeInTheDocument();
+
+      const connectorsIndicator = within(metrics).getByTitle(
+        'Total number of connectors'
+      );
+      expect(connectorsIndicator).toBeInTheDocument();
+      expect(connectorsIndicator).toHaveTextContent(
+        `Connectors ${connectors.length}`
+      );
+
+      const failedConnectorsIndicator = within(metrics).getByTitle(
+        'Number of failed connectors'
+      );
+      expect(failedConnectorsIndicator).toBeInTheDocument();
+      expect(failedConnectorsIndicator).toHaveTextContent(
+        'Failed Connectors 1'
+      );
+
+      const failedTasksIndicator = within(metrics).getByTitle(
+        'Number of failed tasks'
+      );
+      expect(failedTasksIndicator).toBeInTheDocument();
+      expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 1');
+    });
+  });
+});

+ 18 - 38
kafka-ui-react-app/src/components/Connect/New/New.tsx

@@ -4,20 +4,18 @@ import useAppParams from 'lib/hooks/useAppParams';
 import { Controller, FormProvider, useForm } from 'react-hook-form';
 import { ErrorMessage } from '@hookform/error-message';
 import { yupResolver } from '@hookform/resolvers/yup';
-import { Connect } from 'generated-sources';
-import { ClusterName, ConnectName } from 'redux/interfaces';
 import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
 import yup from 'lib/yupExtended';
 import Editor from 'components/common/Editor/Editor';
-import PageLoader from 'components/common/PageLoader/PageLoader';
 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 PageHeading from 'components/common/PageHeading/PageHeading';
-import { createConnector } from 'redux/reducers/connect/connectSlice';
-import { useAppDispatch } from 'lib/hooks/redux';
 import Heading from 'components/common/heading/Heading.styled';
+import { useConnects, useCreateConnector } from 'lib/hooks/api/kafkaConnect';
+import get from 'lodash/get';
+import { Connect } from 'generated-sources';
 
 import * as S from './New.styled';
 
@@ -26,32 +24,24 @@ const validationSchema = yup.object().shape({
   config: yup.string().required().isJsonObject(),
 });
 
-export interface NewProps {
-  fetchConnects(clusterName: ClusterName): unknown;
-  areConnectsFetching: boolean;
-  connects: Connect[];
-}
-
 interface FormValues {
-  connectName: ConnectName;
+  connectName: Connect['name'];
   name: string;
   config: string;
 }
 
-const New: React.FC<NewProps> = ({
-  fetchConnects,
-  areConnectsFetching,
-  connects,
-}) => {
+const New: React.FC = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();
-  const dispatch = useAppDispatch();
   const navigate = useNavigate();
 
+  const { data: connects } = useConnects(clusterName);
+  const mutation = useCreateConnector(clusterName);
+
   const methods = useForm<FormValues>({
     mode: 'onTouched',
     resolver: yupResolver(validationSchema),
     defaultValues: {
-      connectName: connects[0]?.name || '',
+      connectName: get(connects, '0.name', ''),
       name: '',
       config: '',
     },
@@ -64,10 +54,6 @@ const New: React.FC<NewProps> = ({
     setValue,
   } = methods;
 
-  React.useEffect(() => {
-    fetchConnects(clusterName);
-  }, [fetchConnects, clusterName]);
-
   React.useEffect(() => {
     if (connects && connects.length > 0 && !getValues().connectName) {
       setValue('connectName', connects[0].name);
@@ -75,16 +61,14 @@ const New: React.FC<NewProps> = ({
   }, [connects, getValues, setValue]);
 
   const onSubmit = async (values: FormValues) => {
-    const { connector } = await dispatch(
-      createConnector({
-        clusterName,
-        connectName: values.connectName,
-        newConnector: {
-          name: values.name,
-          config: JSON.parse(values.config.trim()),
-        },
-      })
-    ).unwrap();
+    const connector = await mutation.mutateAsync({
+      connectName: values.connectName,
+      newConnector: {
+        name: values.name,
+        config: JSON.parse(values.config.trim()),
+      },
+    });
+
     if (connector) {
       navigate(
         clusterConnectConnectorPath(
@@ -96,11 +80,7 @@ const New: React.FC<NewProps> = ({
     }
   };
 
-  if (areConnectsFetching) {
-    return <PageLoader />;
-  }
-
-  if (connects.length === 0) {
+  if (!connects || connects.length === 0) {
     return null;
   }
 

+ 0 - 20
kafka-ui-react-app/src/components/Connect/New/NewContainer.ts

@@ -1,20 +0,0 @@
-import { connect } from 'react-redux';
-import { fetchConnects } from 'redux/reducers/connect/connectSlice';
-import { RootState } from 'redux/interfaces';
-import {
-  getAreConnectsFetching,
-  getConnects,
-} from 'redux/reducers/connect/selectors';
-
-import New from './New';
-
-const mapStateToProps = (state: RootState) => ({
-  areConnectsFetching: getAreConnectsFetching(state),
-  connects: getConnects(state),
-});
-
-const mapDispatchToProps = {
-  fetchConnects,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(New);

+ 30 - 52
kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx

@@ -4,14 +4,13 @@ import {
   clusterConnectConnectorPath,
   clusterConnectorNewPath,
 } from 'lib/paths';
-import New, { NewProps } from 'components/Connect/New/New';
-import { connects, connector } from 'redux/reducers/connect/__test__/fixtures';
+import New from 'components/Connect/New/New';
+import { connects, connector } from 'lib/fixtures/kafkaConnect';
 import { fireEvent, screen, act } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { ControllerRenderProps } from 'react-hook-form';
-import * as redux from 'react-redux';
+import { useConnects, useCreateConnector } from 'lib/hooks/api/kafkaConnect';
 
-jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
 jest.mock(
   'components/common/Editor/Editor',
   () => (props: ControllerRenderProps) => {
@@ -24,6 +23,10 @@ jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
   useNavigate: () => mockHistoryPush,
 }));
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+  useConnects: jest.fn(),
+  useCreateConnector: jest.fn(),
+}));
 
 describe('New', () => {
   const clusterName = 'my-cluster';
@@ -47,68 +50,43 @@ describe('New', () => {
     });
   };
 
-  const renderComponent = (props: Partial<NewProps> = {}) =>
+  const renderComponent = () =>
     render(
       <WithRoute path={clusterConnectorNewPath()}>
-        <New
-          fetchConnects={jest.fn()}
-          areConnectsFetching={false}
-          connects={connects}
-          {...props}
-        />
+        <New />
       </WithRoute>,
       { initialEntries: [clusterConnectorNewPath(clusterName)] }
     );
 
-  it('fetches connects on mount', async () => {
-    const fetchConnects = jest.fn();
-    await act(() => {
-      renderComponent({ fetchConnects });
-    });
-    expect(fetchConnects).toHaveBeenCalledTimes(1);
-    expect(fetchConnects).toHaveBeenCalledWith(clusterName);
-  });
-
-  it('calls createConnector on form submit', async () => {
-    const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
-    const useDispatchMock = jest.fn(() => ({
-      unwrap: () => ({ connector }),
-    })) as jest.Mock;
-    useDispatchSpy.mockReturnValue(useDispatchMock);
-
-    renderComponent();
-    await simulateFormSubmit();
-
-    expect(useDispatchMock).toHaveBeenCalledTimes(1);
+  beforeEach(() => {
+    (useConnects as jest.Mock).mockImplementation(() => ({
+      data: connects,
+    }));
   });
 
-  it('redirects to connector details view on successful submit', async () => {
-    const route = clusterConnectConnectorPath(
-      clusterName,
-      connects[0].name,
-      connector.name
-    );
-
-    const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
-    const useDispatchMock = jest.fn(() => ({
-      unwrap: () => ({ connector }),
-    })) as jest.Mock;
-    useDispatchSpy.mockReturnValue(useDispatchMock);
-
+  it('calls createConnector on form submit and redirects to the list page on success', async () => {
+    const createConnectorMock = jest.fn(() => {
+      return Promise.resolve(connector);
+    });
+    (useCreateConnector as jest.Mock).mockImplementation(() => ({
+      mutateAsync: createConnectorMock,
+    }));
     renderComponent();
-
     await simulateFormSubmit();
+    expect(createConnectorMock).toHaveBeenCalledTimes(1);
     expect(mockHistoryPush).toHaveBeenCalledTimes(1);
-    expect(mockHistoryPush).toHaveBeenCalledWith(route);
+    expect(mockHistoryPush).toHaveBeenCalledWith(
+      clusterConnectConnectorPath(clusterName, connects[0].name, connector.name)
+    );
   });
 
   it('does not redirect to connector details view on unsuccessful submit', async () => {
-    const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
-    const useDispatchMock = jest.fn(async () => ({
-      unwrap: () => ({}),
-    })) as jest.Mock;
-    useDispatchSpy.mockReturnValue(useDispatchMock);
-
+    const createConnectorMock = jest.fn(() => {
+      return Promise.resolve();
+    });
+    (useCreateConnector as jest.Mock).mockImplementation(() => ({
+      mutateAsync: createConnectorMock,
+    }));
     renderComponent();
     await simulateFormSubmit();
     expect(mockHistoryPush).not.toHaveBeenCalled();

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

@@ -13,22 +13,22 @@ import {
 } from 'lib/paths';
 
 const ConnectCompText = {
-  new: 'NewContainer',
-  list: 'ListContainer',
-  details: 'DetailsContainer',
-  edit: 'EditContainer',
+  new: 'New Page',
+  list: 'List Page',
+  details: 'Details Page',
+  edit: 'Edit Page',
 };
 
-jest.mock('components/Connect/New/NewContainer', () => () => (
+jest.mock('components/Connect/New/New', () => () => (
   <div>{ConnectCompText.new}</div>
 ));
-jest.mock('components/Connect/List/ListContainer', () => () => (
+jest.mock('components/Connect/List/ListPage', () => () => (
   <div>{ConnectCompText.list}</div>
 ));
-jest.mock('components/Connect/Details/DetailsContainer', () => () => (
+jest.mock('components/Connect/Details/DetailsPage', () => () => (
   <div>{ConnectCompText.details}</div>
 ));
-jest.mock('components/Connect/Edit/EditContainer', () => () => (
+jest.mock('components/Connect/Edit/Edit', () => () => (
   <div>{ConnectCompText.edit}</div>
 ));
 
@@ -41,7 +41,7 @@ describe('Connect', () => {
       { initialEntries: [pathname], store }
     );
 
-  it('renders ListContainer', () => {
+  it('renders ListPage', () => {
     renderComponent(
       clusterConnectorsPath('my-cluster'),
       clusterConnectorsPath()
@@ -49,7 +49,7 @@ describe('Connect', () => {
     expect(screen.getByText(ConnectCompText.list)).toBeInTheDocument();
   });
 
-  it('renders NewContainer', () => {
+  it('renders New Page', () => {
     renderComponent(
       clusterConnectorNewPath('my-cluster'),
       clusterConnectorsPath()
@@ -57,7 +57,7 @@ describe('Connect', () => {
     expect(screen.getByText(ConnectCompText.new)).toBeInTheDocument();
   });
 
-  it('renders DetailsContainer', () => {
+  it('renders Details Page', () => {
     renderComponent(
       clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector'),
       clusterConnectsPath()

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

@@ -7,7 +7,7 @@ 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';
-import useClusters from 'lib/hooks/api/useClusters';
+import { useClusters } from 'lib/hooks/api/clusters';
 import { ServerStatus } from 'generated-sources';
 
 import * as S from './ClustersWidget.styled';

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

@@ -1,4 +1,4 @@
-import useClusters from 'lib/hooks/api/useClusters';
+import { useClusters } from 'lib/hooks/api/clusters';
 import React from 'react';
 
 import ClusterMenu from './ClusterMenu';

+ 0 - 5
kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx

@@ -5,7 +5,6 @@ import configureStore from 'redux-mock-store';
 import { RootState } from 'redux/interfaces';
 import * as redux from 'react-redux';
 import { act, screen, waitFor } from '@testing-library/react';
-import fetchMock from 'fetch-mock-jest';
 import {
   clusterTopicCopyPath,
   clusterTopicNewPath,
@@ -58,10 +57,6 @@ const renderComponent = (path: string, store = storeMock) => {
 };
 
 describe('New', () => {
-  beforeEach(() => {
-    fetchMock.reset();
-  });
-
   afterEach(() => {
     mockNavigate.mockClear();
   });

+ 9 - 1
kafka-ui-react-app/src/components/common/Metrics/Indicator.tsx

@@ -30,7 +30,15 @@ const Indicator: React.FC<PropsWithChildren<Props>> = ({
         )}
       </S.IndicatorTitle>
       <span>
-        {fetching ? <i className="fas fa-spinner fa-pulse" /> : children}
+        {fetching ? (
+          <i
+            className="fas fa-spinner fa-pulse"
+            role="progressbar"
+            aria-label="Loading"
+          />
+        ) : (
+          children
+        )}
       </span>
     </div>
   </S.IndicatorWrapper>

+ 1 - 1
kafka-ui-react-app/src/components/common/Metrics/Section.tsx

@@ -7,7 +7,7 @@ interface Props {
 }
 
 const Section: React.FC<PropsWithChildren<Props>> = ({ title, children }) => (
-  <div>
+  <div role="group">
     {title && <S.SectionTitle>{title}</S.SectionTitle>}
     <S.IndicatorsWrapper>{children}</S.IndicatorsWrapper>
   </div>

+ 1 - 6
kafka-ui-react-app/src/index.tsx

@@ -2,14 +2,11 @@ import React from 'react';
 import { createRoot } from 'react-dom/client';
 import { BrowserRouter } from 'react-router-dom';
 import { Provider } from 'react-redux';
-import { QueryClient, QueryClientProvider } from 'react-query';
 import App from 'components/App';
 import { store } from 'redux/store';
 import 'theme/index.scss';
 import 'lib/constants';
 
-const queryClient = new QueryClient();
-
 const container =
   document.getElementById('root') || document.createElement('div');
 const root = createRoot(container);
@@ -17,9 +14,7 @@ const root = createRoot(container);
 root.render(
   <Provider store={store}>
     <BrowserRouter basename={window.basePath || '/'}>
-      <QueryClientProvider client={queryClient}>
-        <App />
-      </QueryClientProvider>
+      <App />
     </BrowserRouter>
   </Provider>
 );

+ 6 - 111
kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts → kafka-ui-react-app/src/lib/fixtures/kafkaConnect.ts

@@ -13,35 +13,6 @@ export const connects: Connect[] = [
   { name: 'second', address: 'localhost:8084' },
 ];
 
-export const connectorsServerPayload = [
-  {
-    connect: 'first',
-    name: 'hdfs-source-connector',
-    connector_class: 'FileStreamSource',
-    type: ConnectorType.SOURCE,
-    topics: ['a', 'b', 'c'],
-    status: {
-      state: ConnectorTaskStatus.RUNNING,
-      workerId: 1,
-    },
-    tasks_count: 2,
-    failed_tasks_count: 0,
-  },
-  {
-    connect: 'second',
-    name: 'hdfs2-source-connector',
-    connector_class: 'FileStreamSource',
-    type: ConnectorType.SINK,
-    topics: ['test-topic'],
-    status: {
-      state: ConnectorTaskStatus.FAILED,
-      workerId: 1,
-    },
-    tasks_count: 3,
-    failed_tasks_count: 1,
-  },
-];
-
 export const connectors: FullConnectorInfo[] = [
   {
     connect: 'first',
@@ -69,50 +40,6 @@ export const connectors: FullConnectorInfo[] = [
   },
 ];
 
-export const failedConnectors: FullConnectorInfo[] = [
-  {
-    connect: 'first',
-    name: 'hdfs-source-connector',
-    connectorClass: 'FileStreamSource',
-    type: ConnectorType.SOURCE,
-    topics: ['a', 'b', 'c'],
-    status: {
-      state: ConnectorState.FAILED,
-    },
-    tasksCount: 2,
-    failedTasksCount: 0,
-  },
-  {
-    connect: 'second',
-    name: 'hdfs2-source-connector',
-    connectorClass: 'FileStreamSource',
-    type: ConnectorType.SINK,
-    topics: ['a', 'b', 'c'],
-    status: {
-      state: ConnectorState.FAILED,
-    },
-    tasksCount: 3,
-    failedTasksCount: 1,
-  },
-];
-
-export const connectorServerPayload = {
-  connect: 'first',
-  name: 'hdfs-source-connector',
-  type: ConnectorType.SOURCE,
-  status: {
-    state: ConnectorTaskStatus.RUNNING,
-    worker_id: 'kafka-connect0:8083',
-  },
-  config: {
-    'connector.class': 'FileStreamSource',
-    'tasks.max': '10',
-    topic: 'test-topic',
-    file: '/some/file',
-  },
-  tasks: [{ connector: 'first', task: 1 }],
-};
-
 export const connector: Connector = {
   connect: 'first',
   name: 'hdfs-source-connector',
@@ -130,13 +57,13 @@ export const connector: Connector = {
   tasks: [{ connector: 'first', task: 1 }],
 };
 
-export const tasksServerPayload = [
+export const tasks: Task[] = [
   {
     id: { connector: 'first', task: 1 },
     status: {
       id: 1,
       state: ConnectorTaskStatus.RUNNING,
-      worker_id: 'kafka-connect0:8083',
+      workerId: 'kafka-connect0:8083',
     },
     config: {
       'batch.size': '2000',
@@ -151,7 +78,7 @@ export const tasksServerPayload = [
       id: 2,
       state: ConnectorTaskStatus.FAILED,
       trace: 'Failure 1',
-      worker_id: 'kafka-connect0:8083',
+      workerId: 'kafka-connect0:8083',
     },
     config: {
       'batch.size': '1000',
@@ -165,7 +92,7 @@ export const tasksServerPayload = [
     status: {
       id: 3,
       state: ConnectorTaskStatus.RUNNING,
-      worker_id: 'kafka-connect0:8083',
+      workerId: 'kafka-connect0:8083',
     },
     config: {
       'batch.size': '3000',
@@ -174,43 +101,11 @@ export const tasksServerPayload = [
       topic: 'test-topic',
     },
   },
-];
-
-export const tasks: Task[] = [
   {
-    id: { connector: 'first', task: 1 },
-    status: {
-      id: 1,
-      state: ConnectorTaskStatus.RUNNING,
-      workerId: 'kafka-connect0:8083',
-    },
-    config: {
-      'batch.size': '2000',
-      file: '/some/file',
-      'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
-      topic: 'test-topic',
-    },
-  },
-  {
-    id: { connector: 'first', task: 2 },
-    status: {
-      id: 2,
-      state: ConnectorTaskStatus.FAILED,
-      trace: 'Failure 1',
-      workerId: 'kafka-connect0:8083',
-    },
-    config: {
-      'batch.size': '1000',
-      file: '/some/file2',
-      'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
-      topic: 'test-topic',
-    },
-  },
-  {
-    id: { connector: 'first', task: 3 },
+    id: { connector: 'first', task: 4 },
     status: {
       id: 3,
-      state: ConnectorTaskStatus.RUNNING,
+      state: ConnectorTaskStatus.PAUSED,
       workerId: 'kafka-connect0:8083',
     },
     config: {

+ 168 - 0
kafka-ui-react-app/src/lib/hooks/api/__tests__/kafkaConnect.spec.ts

@@ -0,0 +1,168 @@
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { renderQueryHook, TestQueryClientProvider } from 'lib/testHelpers';
+import * as hooks from 'lib/hooks/api/kafkaConnect';
+import fetchMock from 'fetch-mock';
+import { connectors, connects, tasks } from 'lib/fixtures/kafkaConnect';
+import { UseQueryResult } from 'react-query';
+import { ConnectorAction } from 'generated-sources';
+
+const clusterName = 'test-cluster';
+const connectName = 'test-connect';
+const connectorName = 'test-connector';
+
+const connectsPath = `/api/clusters/${clusterName}/connects`;
+const connectorsPath = `/api/clusters/${clusterName}/connectors`;
+const connectorPath = `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`;
+
+const connectorProps = {
+  clusterName,
+  connectName,
+  connectorName,
+};
+
+const expectQueryWorks = async (
+  mock: fetchMock.FetchMockStatic,
+  result: { current: UseQueryResult<unknown, unknown> }
+) => {
+  await waitFor(() => expect(result.current.isFetched).toBeTruthy());
+  expect(mock.calls()).toHaveLength(1);
+  expect(result.current.data).toBeDefined();
+};
+
+describe('kafkaConnect hooks', () => {
+  beforeEach(() => fetchMock.restore());
+  describe('useConnects', () => {
+    it('returns the correct data', async () => {
+      const mock = fetchMock.getOnce(connectsPath, connects);
+      const { result } = renderQueryHook(() => hooks.useConnects(clusterName));
+      await expectQueryWorks(mock, result);
+    });
+  });
+  describe('useConnectors', () => {
+    it('returns the correct data', async () => {
+      const mock = fetchMock.getOnce(connectorsPath, connectors);
+      const { result } = renderQueryHook(() =>
+        hooks.useConnectors(clusterName)
+      );
+      await expectQueryWorks(mock, result);
+    });
+
+    it('returns the correct data for request with search criteria', async () => {
+      const search = 'test-search';
+      const mock = fetchMock.getOnce(
+        `${connectorsPath}?search=${search}`,
+        connectors
+      );
+      const { result } = renderQueryHook(() =>
+        hooks.useConnectors(clusterName, search)
+      );
+      await expectQueryWorks(mock, result);
+    });
+  });
+  describe('useConnector', () => {
+    it('returns the correct data', async () => {
+      const mock = fetchMock.getOnce(connectorPath, connectors[0]);
+      const { result } = renderQueryHook(() =>
+        hooks.useConnector(connectorProps)
+      );
+      await expectQueryWorks(mock, result);
+    });
+  });
+  describe('useConnectorTasks', () => {
+    it('returns the correct data', async () => {
+      const mock = fetchMock.getOnce(`${connectorPath}/tasks`, tasks);
+      const { result } = renderQueryHook(() =>
+        hooks.useConnectorTasks(connectorProps)
+      );
+      await expectQueryWorks(mock, result);
+    });
+  });
+  describe('useConnectorConfig', () => {
+    it('returns the correct data', async () => {
+      const mock = fetchMock.getOnce(`${connectorPath}/config`, {});
+      const { result } = renderQueryHook(() =>
+        hooks.useConnectorConfig(connectorProps)
+      );
+      await expectQueryWorks(mock, result);
+    });
+  });
+
+  describe('mutatations', () => {
+    describe('useUpdateConnectorState', () => {
+      it('returns the correct data', async () => {
+        const action = ConnectorAction.RESTART;
+        const uri = `${connectorPath}/action/${action}`;
+        const mock = fetchMock.postOnce(uri, connectors[0]);
+        const { result } = renderHook(
+          () => hooks.useUpdateConnectorState(connectorProps),
+          { wrapper: TestQueryClientProvider }
+        );
+        await act(() => result.current.mutateAsync(action));
+        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());
+        expect(mock.calls()).toHaveLength(1);
+      });
+    });
+    describe('useRestartConnectorTask', () => {
+      it('returns the correct data', async () => {
+        const taskId = 123456;
+        const uri = `${connectorPath}/tasks/${taskId}/action/restart`;
+        const mock = fetchMock.postOnce(uri, {});
+        const { result } = renderHook(
+          () => hooks.useRestartConnectorTask(connectorProps),
+          { wrapper: TestQueryClientProvider }
+        );
+        await act(() => result.current.mutateAsync(taskId));
+        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());
+        expect(mock.calls()).toHaveLength(1);
+      });
+    });
+    describe('useUpdateConnectorConfig', () => {
+      it('returns the correct data', async () => {
+        const mock = fetchMock.putOnce(`${connectorPath}/config`, {});
+        const { result } = renderHook(
+          () => hooks.useUpdateConnectorConfig(connectorProps),
+          { wrapper: TestQueryClientProvider }
+        );
+        await act(async () => {
+          await result.current.mutateAsync({ config: 1 });
+        });
+        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());
+        expect(mock.calls()).toHaveLength(1);
+      });
+    });
+    describe('useCreateConnector', () => {
+      it('returns the correct data', async () => {
+        const mock = fetchMock.postOnce(
+          `${connectsPath}/${connectName}/connectors`,
+          {}
+        );
+        const { result } = renderHook(
+          () => hooks.useCreateConnector(clusterName),
+          { wrapper: TestQueryClientProvider }
+        );
+        await act(async () => {
+          await result.current.mutateAsync({
+            connectName,
+            newConnector: { name: connectorName, config: { a: 1 } },
+          });
+        });
+        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());
+        expect(mock.calls()).toHaveLength(1);
+      });
+    });
+    describe('useDeleteConnector', () => {
+      it('returns the correct data', async () => {
+        const mock = fetchMock.deleteOnce(connectorPath, {});
+        const { result } = renderHook(
+          () => hooks.useDeleteConnector(connectorProps),
+          { wrapper: TestQueryClientProvider }
+        );
+        await act(async () => {
+          await result.current.mutateAsync();
+        });
+        await waitFor(() => expect(result.current.isSuccess).toBeTruthy());
+        expect(mock.calls()).toHaveLength(1);
+      });
+    });
+  });
+});

+ 35 - 0
kafka-ui-react-app/src/lib/hooks/api/brokers.ts

@@ -0,0 +1,35 @@
+import { brokersApiClient as api } from 'lib/api';
+import { useQuery } from 'react-query';
+import { ClusterName } from 'redux/interfaces';
+
+export function useBrokers(clusterName: ClusterName) {
+  return useQuery(
+    ['clusters', clusterName, 'brokers'],
+    () => api.getBrokers({ clusterName }),
+    { refetchInterval: 5000 }
+  );
+}
+
+export function useBrokerMetrics(clusterName: ClusterName, brokerId: number) {
+  return useQuery(
+    ['clusters', clusterName, 'brokers', brokerId, 'metrics'],
+    () =>
+      api.getBrokersMetrics({
+        clusterName,
+        id: brokerId,
+      }),
+    { refetchInterval: 5000 }
+  );
+}
+
+export function useBrokerLogDirs(clusterName: ClusterName, brokerId: number) {
+  return useQuery(
+    ['clusters', clusterName, 'brokers', brokerId, 'logDirs'],
+    () =>
+      api.getAllBrokersLogdirs({
+        clusterName,
+        broker: [brokerId],
+      }),
+    { refetchInterval: 5000 }
+  );
+}

+ 14 - 0
kafka-ui-react-app/src/lib/hooks/api/clusters.ts

@@ -0,0 +1,14 @@
+import { clustersApiClient as api } from 'lib/api';
+import { useQuery } from 'react-query';
+import { ClusterName } from 'redux/interfaces';
+
+export function useClusters() {
+  return useQuery(['clusters'], () => api.getClusters());
+}
+export function useClusterStats(clusterName: ClusterName) {
+  return useQuery(
+    ['clusterStats', clusterName],
+    () => api.getClusterStats({ clusterName }),
+    { refetchInterval: 5000 }
+  );
+}

+ 128 - 0
kafka-ui-react-app/src/lib/hooks/api/kafkaConnect.ts

@@ -0,0 +1,128 @@
+import {
+  Connect,
+  Connector,
+  ConnectorAction,
+  NewConnector,
+} from 'generated-sources';
+import { kafkaConnectApiClient as api } from 'lib/api';
+import sortBy from 'lodash/sortBy';
+import { useMutation, useQuery, useQueryClient } from 'react-query';
+import { ClusterName } from 'redux/interfaces';
+
+interface UseConnectorProps {
+  clusterName: ClusterName;
+  connectName: Connect['name'];
+  connectorName: Connector['name'];
+}
+interface CreateConnectorProps {
+  connectName: Connect['name'];
+  newConnector: NewConnector;
+}
+
+const connectsKey = (clusterName: ClusterName) => [
+  'clusters',
+  clusterName,
+  'connects',
+];
+const connectorsKey = (clusterName: ClusterName, search?: string) => {
+  const base = ['clusters', clusterName, 'connectors'];
+  if (search) {
+    return [...base, { search }];
+  }
+  return base;
+};
+const connectorKey = (props: UseConnectorProps) => [
+  'clusters',
+  props.clusterName,
+  'connects',
+  props.connectName,
+  'connectors',
+  props.connectorName,
+];
+const connectorTasksKey = (props: UseConnectorProps) => [
+  ...connectorKey(props),
+  'tasks',
+];
+const connectorConfigKey = (props: UseConnectorProps) => [
+  ...connectorKey(props),
+  'config',
+];
+
+export function useConnects(clusterName: ClusterName) {
+  return useQuery(connectsKey(clusterName), () =>
+    api.getConnects({ clusterName })
+  );
+}
+export function useConnectors(clusterName: ClusterName, search?: string) {
+  return useQuery(
+    connectorsKey(clusterName, search),
+    () => api.getAllConnectors({ clusterName, search }),
+    {
+      select: (data) => sortBy(data, 'name'),
+    }
+  );
+}
+export function useConnector(props: UseConnectorProps) {
+  return useQuery(connectorKey(props), () => api.getConnector(props));
+}
+export function useConnectorTasks(props: UseConnectorProps) {
+  return useQuery(
+    connectorTasksKey(props),
+    () => api.getConnectorTasks(props),
+    {
+      select: (data) => sortBy(data, 'status.id'),
+    }
+  );
+}
+export function useUpdateConnectorState(props: UseConnectorProps) {
+  const client = useQueryClient();
+  return useMutation(
+    (action: ConnectorAction) => api.updateConnectorState({ ...props, action }),
+    {
+      onSuccess: () => client.invalidateQueries(connectorKey(props)),
+    }
+  );
+}
+export function useRestartConnectorTask(props: UseConnectorProps) {
+  const client = useQueryClient();
+  return useMutation(
+    (taskId: number) => api.restartConnectorTask({ ...props, taskId }),
+    {
+      onSuccess: () => client.invalidateQueries(connectorTasksKey(props)),
+    }
+  );
+}
+export function useConnectorConfig(props: UseConnectorProps) {
+  return useQuery([...connectorKey(props), 'config'], () =>
+    api.getConnectorConfig(props)
+  );
+}
+export function useUpdateConnectorConfig(props: UseConnectorProps) {
+  const client = useQueryClient();
+  return useMutation(
+    (requestBody: Connector['config']) =>
+      api.setConnectorConfig({ ...props, requestBody }),
+    {
+      onSuccess: () => {
+        client.invalidateQueries(connectorKey(props));
+        client.invalidateQueries(connectorConfigKey(props));
+      },
+    }
+  );
+}
+export function useCreateConnector(clusterName: ClusterName) {
+  const client = useQueryClient();
+  return useMutation(
+    (props: CreateConnectorProps) =>
+      api.createConnector({ ...props, clusterName }),
+    {
+      onSuccess: () => client.invalidateQueries(connectorsKey(clusterName)),
+    }
+  );
+}
+export function useDeleteConnector(props: UseConnectorProps) {
+  const client = useQueryClient();
+  return useMutation(() => api.deleteConnector(props), {
+    onSuccess: () => client.invalidateQueries(connectorsKey(props.clusterName)),
+  });
+}

+ 0 - 8
kafka-ui-react-app/src/lib/hooks/api/useClusters.ts

@@ -1,8 +0,0 @@
-import { clustersApiClient } from 'lib/api';
-import { useQuery } from 'react-query';
-
-export default function useClusters() {
-  return useQuery(['clusters'], () => clustersApiClient.getClusters(), {
-    suspense: true,
-  });
-}

+ 0 - 11
kafka-ui-react-app/src/lib/hooks/useBrokers.tsx

@@ -1,11 +0,0 @@
-import { brokersApiClient } from 'lib/api';
-import { useQuery } from 'react-query';
-import { ClusterName } from 'redux/interfaces';
-
-export default function useBrokers(clusterName: ClusterName) {
-  return useQuery(
-    ['brokers', clusterName],
-    () => brokersApiClient.getBrokers({ clusterName }),
-    { suspense: true, refetchInterval: 5000 }
-  );
-}

+ 0 - 18
kafka-ui-react-app/src/lib/hooks/useBrokersLogDirs.tsx

@@ -1,18 +0,0 @@
-import { brokersApiClient } from 'lib/api';
-import { useQuery } from 'react-query';
-import { ClusterName } from 'redux/interfaces';
-
-export default function useBrokersLogDirs(
-  clusterName: ClusterName,
-  brokerId: number
-) {
-  return useQuery(
-    ['logDirs', clusterName, brokerId],
-    () =>
-      brokersApiClient.getAllBrokersLogdirs({
-        clusterName,
-        broker: [brokerId],
-      }),
-    { suspense: true, refetchInterval: 5000 }
-  );
-}

+ 0 - 18
kafka-ui-react-app/src/lib/hooks/useBrokersMetrics.tsx

@@ -1,18 +0,0 @@
-import { brokersApiClient } from 'lib/api';
-import { useQuery } from 'react-query';
-import { ClusterName } from 'redux/interfaces';
-
-export default function useBrokersMetrics(
-  clusterName: ClusterName,
-  brokerId: number
-) {
-  return useQuery(
-    ['metrics', clusterName, brokerId],
-    () =>
-      brokersApiClient.getBrokersMetrics({
-        clusterName,
-        id: brokerId,
-      }),
-    { suspense: true, refetchInterval: 5000 }
-  );
-}

+ 0 - 11
kafka-ui-react-app/src/lib/hooks/useClusterStats.tsx

@@ -1,11 +0,0 @@
-import { clustersApiClient } from 'lib/api';
-import { useQuery } from 'react-query';
-import { ClusterName } from 'redux/interfaces';
-
-export default function useClusterStats(clusterName: ClusterName) {
-  return useQuery(
-    ['clusterStats', clusterName],
-    () => clustersApiClient.getClusterStats({ clusterName }),
-    { suspense: true, refetchInterval: 5000 }
-  );
-}

+ 13 - 13
kafka-ui-react-app/src/lib/paths.ts

@@ -1,8 +1,7 @@
+import { Connect, Connector } from 'generated-sources';
 import {
   BrokerId,
   ClusterName,
-  ConnectName,
-  ConnectorName,
   ConsumerGroupID,
   SchemaName,
   TopicName,
@@ -199,18 +198,18 @@ export const clusterConnectorNewPath = (
 ) => `${clusterConnectorsPath(clusterName)}/create-new`;
 export const clusterConnectConnectorsPath = (
   clusterName: ClusterName = RouteParams.clusterName,
-  connectName: ConnectName = RouteParams.connectName
+  connectName: Connect['name'] = RouteParams.connectName
 ) => `${clusterConnectsPath(clusterName)}/${connectName}/connectors`;
 export const clusterConnectConnectorPath = (
   clusterName: ClusterName = RouteParams.clusterName,
-  connectName: ConnectName = RouteParams.connectName,
-  connectorName: ConnectorName = RouteParams.connectorName
+  connectName: Connect['name'] = RouteParams.connectName,
+  connectorName: Connector['name'] = RouteParams.connectorName
 ) =>
   `${clusterConnectConnectorsPath(clusterName, connectName)}/${connectorName}`;
 export const clusterConnectConnectorEditPath = (
   clusterName: ClusterName = RouteParams.clusterName,
-  connectName: ConnectName = RouteParams.connectName,
-  connectorName: ConnectorName = RouteParams.connectorName
+  connectName: Connect['name'] = RouteParams.connectName,
+  connectorName: Connector['name'] = RouteParams.connectorName
 ) =>
   `${clusterConnectConnectorsPath(
     clusterName,
@@ -218,8 +217,8 @@ export const clusterConnectConnectorEditPath = (
   )}/${connectorName}/edit`;
 export const clusterConnectConnectorTasksPath = (
   clusterName: ClusterName = RouteParams.clusterName,
-  connectName: ConnectName = RouteParams.connectName,
-  connectorName: ConnectorName = RouteParams.connectorName
+  connectName: Connect['name'] = RouteParams.connectName,
+  connectorName: Connector['name'] = RouteParams.connectorName
 ) =>
   `${clusterConnectConnectorPath(
     clusterName,
@@ -228,18 +227,19 @@ export const clusterConnectConnectorTasksPath = (
   )}/${clusterConnectConnectorTasksRelativePath}`;
 export const clusterConnectConnectorConfigPath = (
   clusterName: ClusterName = RouteParams.clusterName,
-  connectName: ConnectName = RouteParams.connectName,
-  connectorName: ConnectorName = RouteParams.connectorName
+  connectName: Connect['name'] = RouteParams.connectName,
+  connectorName: Connector['name'] = RouteParams.connectorName
 ) =>
   `${clusterConnectConnectorPath(
     clusterName,
     connectName,
     connectorName
   )}/${clusterConnectConnectorConfigRelativePath}`;
+
 export type RouterParamsClusterConnectConnector = {
   clusterName: ClusterName;
-  connectName: ConnectName;
-  connectorName: ConnectorName;
+  connectName: Connect['name'];
+  connectorName: Connector['name'];
 };
 
 // KsqlDb

+ 31 - 38
kafka-ui-react-app/src/lib/testHelpers.tsx

@@ -8,13 +8,13 @@ import {
 import { Provider } from 'react-redux';
 import { ThemeProvider } from 'styled-components';
 import theme from 'theme/theme';
-import { render, RenderOptions, screen } from '@testing-library/react';
+import { render, renderHook, RenderOptions } from '@testing-library/react';
 import { AnyAction, Store } from 'redux';
 import { RootState } from 'redux/interfaces';
 import { configureStore } from '@reduxjs/toolkit';
 import rootReducer from 'redux/reducers';
 import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
-import { QueryClient, QueryClientProvider } from 'react-query';
+import { QueryClient, QueryClientProvider, UseQueryResult } from 'react-query';
 
 interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
   preloadedState?: Partial<RootState>;
@@ -22,23 +22,12 @@ interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
   initialEntries?: MemoryRouterProps['initialEntries'];
 }
 
-export function getByTextContent(textMatch: string | RegExp): HTMLElement {
-  return screen.getByText((content, node) => {
-    const hasText = (nod: Element) => nod.textContent === textMatch;
-    const nodeHasText = hasText(node as Element);
-    const childrenDontHaveText = Array.from(node?.children || []).every(
-      (child) => !hasText(child)
-    );
-    return nodeHasText && childrenDontHaveText;
-  });
-}
-
-interface WithRouterProps {
+interface WithRouteProps {
   children: React.ReactNode;
   path: string;
 }
 
-export const WithRoute: React.FC<WithRouterProps> = ({ children, path }) => {
+export const WithRoute: React.FC<WithRouteProps> = ({ children, path }) => {
   return (
     <Routes>
       <Route path={path} element={children} />
@@ -46,6 +35,18 @@ export const WithRoute: React.FC<WithRouterProps> = ({ children, path }) => {
   );
 };
 
+export const TestQueryClientProvider: React.FC<PropsWithChildren<unknown>> = ({
+  children,
+}) => {
+  // use new QueryClient instance for each test run to avoid issues with cache
+  const queryClient = new QueryClient({
+    defaultOptions: { queries: { retry: false } },
+  });
+  return (
+    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
+  );
+};
+
 const customRender = (
   ui: ReactElement,
   {
@@ -58,30 +59,27 @@ const customRender = (
     ...renderOptions
   }: CustomRenderOptions = {}
 ) => {
-  // use new QueryClient instance for each test run to avoid issues with cache
-  const queryClient = new QueryClient({
-    defaultOptions: { queries: { retry: false } },
-  });
   // overrides @testing-library/react render.
   const AllTheProviders: React.FC<PropsWithChildren<unknown>> = ({
     children,
-  }) => {
-    return (
-      <ThemeProvider theme={theme}>
-        <Provider store={store}>
-          <QueryClientProvider client={queryClient}>
-            <MemoryRouter initialEntries={initialEntries}>
-              {children}
-            </MemoryRouter>
-          </QueryClientProvider>
-        </Provider>
-      </ThemeProvider>
-    );
-  };
+  }) => (
+    <ThemeProvider theme={theme}>
+      <Provider store={store}>
+        <TestQueryClientProvider>
+          <MemoryRouter initialEntries={initialEntries}>
+            {children}
+          </MemoryRouter>
+        </TestQueryClientProvider>
+      </Provider>
+    </ThemeProvider>
+  );
   return render(ui, { wrapper: AllTheProviders, ...renderOptions });
 };
 
-export { customRender as render };
+const customRenderHook = (hook: () => UseQueryResult<unknown, unknown>) =>
+  renderHook(hook, { wrapper: TestQueryClientProvider });
+
+export { customRender as render, customRenderHook as renderQueryHook };
 
 export class EventSourceMock {
   url: string;
@@ -106,8 +104,3 @@ export class EventSourceMock {
 export const getTypeAndPayload = (store: typeof mockStoreCreator) => {
   return store.getActions().map(({ type, payload }) => ({ type, payload }));
 };
-
-export const getAlertActions = (mockStore: typeof mockStoreCreator) =>
-  getTypeAndPayload(mockStore).filter((currentAction: AnyAction) =>
-    currentAction.type.startsWith('alerts')
-  );

+ 0 - 47
kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts

@@ -1,47 +0,0 @@
-import {
-  ClusterStats,
-  CompatibilityLevelCompatibilityEnum,
-  NewSchemaSubject,
-  SchemaSubject,
-  SchemaType,
-  SortOrder,
-} from 'generated-sources';
-
-export const clusterStats: ClusterStats = {
-  brokerCount: 1,
-  activeControllers: 1,
-  onlinePartitionCount: 6,
-  offlinePartitionCount: 0,
-  inSyncReplicasCount: 6,
-  outOfSyncReplicasCount: 0,
-  underReplicatedPartitionCount: 0,
-  diskUsage: [{ brokerId: 1, segmentSize: 6538, segmentCount: 6 }],
-};
-
-export const schemaPayload: NewSchemaSubject = {
-  subject: 'NewSchema',
-  schema:
-    '{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-  schemaType: SchemaType.JSON,
-};
-
-export const schema: SchemaSubject = {
-  subject: 'NewSchema',
-  schema:
-    '{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-  schemaType: SchemaType.JSON,
-  version: '1',
-  id: 1,
-  compatibilityLevel: CompatibilityLevelCompatibilityEnum.BACKWARD,
-};
-
-export const mockTopicsState = {
-  byName: {},
-  allNames: [],
-  totalPages: 1,
-  messages: [],
-  search: '',
-  orderBy: null,
-  sortOrder: SortOrder.ASC,
-  consumerGroups: [],
-};

+ 0 - 23
kafka-ui-react-app/src/redux/interfaces/connect.ts

@@ -1,23 +0,0 @@
-import { Connect, Connector, FullConnectorInfo, Task } from 'generated-sources';
-
-import { ClusterName } from './cluster';
-
-export type ConnectName = Connect['name'];
-export type ConnectorName = Connector['name'];
-export type ConnectorConfig = Connector['config'];
-
-export interface ConnectState {
-  connects: Connect[];
-  connectors: FullConnectorInfo[];
-  currentConnector: {
-    connector: Connector | null;
-    tasks: Task[];
-    config: ConnectorConfig | null;
-  };
-  search: string;
-}
-
-export interface ConnectorSearch {
-  clusterName: ClusterName;
-  search: string;
-}

+ 0 - 2
kafka-ui-react-app/src/redux/interfaces/index.ts

@@ -8,8 +8,6 @@ export * from './consumerGroup';
 export * from './schema';
 export * from './loader';
 export * from './alerts';
-export * from './connect';
 
 export type RootState = ReturnType<typeof rootReducer>;
-export type AppStore = ReturnType<typeof store.getState>;
 export type AppDispatch = typeof store.dispatch;

+ 0 - 766
kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts

@@ -1,766 +0,0 @@
-import {
-  ConnectorState,
-  ConnectorTaskStatus,
-  ConnectorAction,
-} from 'generated-sources';
-import reducer, {
-  initialState,
-  fetchConnects,
-  fetchConnectors,
-  fetchConnector,
-  createConnector,
-  deleteConnector,
-  setConnectorStatusState,
-  fetchConnectorTasks,
-  fetchConnectorConfig,
-  updateConnectorConfig,
-  restartConnector,
-  pauseConnector,
-  resumeConnector,
-  restartConnectorTask,
-} from 'redux/reducers/connect/connectSlice';
-import fetchMock from 'fetch-mock-jest';
-import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
-import { getTypeAndPayload, getAlertActions } from 'lib/testHelpers';
-
-import {
-  connects,
-  connectors,
-  connector,
-  tasks,
-  connectorsServerPayload,
-  connectorServerPayload,
-  tasksServerPayload,
-} from './fixtures';
-
-const runningConnectorState = {
-  ...initialState,
-  currentConnector: {
-    ...initialState.currentConnector,
-    connector: {
-      ...connector,
-      status: {
-        ...connector.status,
-        state: ConnectorState.RUNNING,
-      },
-    },
-    tasks: tasks.map((task) => ({
-      ...task,
-      status: {
-        ...task.status,
-        state: ConnectorTaskStatus.RUNNING,
-      },
-    })),
-  },
-};
-
-const pausedConnectorState = {
-  ...initialState,
-  currentConnector: {
-    ...initialState.currentConnector,
-    connector: {
-      ...connector,
-      status: {
-        ...connector.status,
-        state: ConnectorState.PAUSED,
-      },
-    },
-    tasks: tasks.map((task) => ({
-      ...task,
-      status: {
-        ...task.status,
-        state: ConnectorTaskStatus.PAUSED,
-      },
-    })),
-  },
-};
-
-describe('Connect slice', () => {
-  describe('Reducer', () => {
-    it('reacts on fetchConnects/fulfilled', () => {
-      expect(
-        reducer(initialState, {
-          type: fetchConnects.fulfilled,
-          payload: { connects },
-        })
-      ).toEqual({
-        ...initialState,
-        connects,
-      });
-    });
-
-    it('reacts on fetchConnectors/fulfilled', () => {
-      expect(
-        reducer(initialState, {
-          type: fetchConnectors.fulfilled,
-          payload: { connectors },
-        })
-      ).toEqual({
-        ...initialState,
-        connectors,
-      });
-    });
-
-    it('reacts on fetchConnector/fulfilled', () => {
-      expect(
-        reducer(initialState, {
-          type: fetchConnector.fulfilled,
-          payload: { connector },
-        })
-      ).toEqual({
-        ...initialState,
-        currentConnector: {
-          ...initialState.currentConnector,
-          connector,
-        },
-      });
-    });
-
-    it('reacts on createConnector/fulfilled', () => {
-      expect(
-        reducer(initialState, {
-          type: createConnector.fulfilled,
-          payload: { connector },
-        })
-      ).toEqual({
-        ...initialState,
-        currentConnector: {
-          ...initialState.currentConnector,
-          connector,
-        },
-      });
-    });
-
-    it('reacts on deleteConnector/fulfilled', () => {
-      expect(
-        reducer(
-          { ...initialState, connectors },
-          {
-            type: deleteConnector.fulfilled,
-            payload: { connectorName: connectors[0].name },
-          }
-        )
-      ).toEqual({
-        ...initialState,
-        connectors: connectors.slice(1),
-      });
-    });
-
-    it('reacts on setConnectorStatusState/fulfilled', () => {
-      expect(
-        reducer(runningConnectorState, {
-          type: setConnectorStatusState,
-          payload: {
-            taskState: ConnectorTaskStatus.PAUSED,
-            connectorState: ConnectorState.PAUSED,
-          },
-        })
-      ).toEqual(pausedConnectorState);
-    });
-
-    it('reacts on fetchConnectorTasks/fulfilled', () => {
-      expect(
-        reducer(initialState, {
-          type: fetchConnectorTasks.fulfilled,
-          payload: { tasks },
-        })
-      ).toEqual({
-        ...initialState,
-        currentConnector: {
-          ...initialState.currentConnector,
-          tasks,
-        },
-      });
-    });
-
-    it('reacts on fetchConnectorConfig/fulfilled', () => {
-      expect(
-        reducer(initialState, {
-          type: fetchConnectorConfig.fulfilled,
-          payload: { config: connector.config },
-        })
-      ).toEqual({
-        ...initialState,
-        currentConnector: {
-          ...initialState.currentConnector,
-          config: connector.config,
-        },
-      });
-    });
-
-    it('reacts on updateConnectorConfig/fulfilled', () => {
-      expect(
-        reducer(
-          {
-            ...initialState,
-            currentConnector: {
-              ...initialState.currentConnector,
-              config: {
-                ...connector.config,
-                fieldToRemove: 'Fake',
-              },
-            },
-          },
-          {
-            type: updateConnectorConfig.fulfilled,
-            payload: { connector },
-          }
-        )
-      ).toEqual({
-        ...initialState,
-        currentConnector: {
-          ...initialState.currentConnector,
-          connector,
-          config: connector.config,
-        },
-      });
-    });
-  });
-
-  describe('Thunks', () => {
-    const store = mockStoreCreator;
-    const clusterName = 'local';
-    const connectName = 'first';
-    const connectorName = 'hdfs-source-connector';
-    const taskId = 10;
-
-    describe('Thunks', () => {
-      afterEach(() => {
-        fetchMock.restore();
-        store.clearActions();
-      });
-      describe('fetchConnects', () => {
-        it('creates fetchConnects/fulfilled when fetching connects', async () => {
-          fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, connects);
-          await store.dispatch(fetchConnects(clusterName));
-
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnects.pending.type },
-            {
-              type: fetchConnects.fulfilled.type,
-              payload: { connects },
-            },
-          ]);
-        });
-        it('creates fetchConnects/rejected', async () => {
-          fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, 404);
-          await store.dispatch(fetchConnects(clusterName));
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnects.pending.type },
-            {
-              type: fetchConnects.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-      describe('fetchConnectors', () => {
-        it('creates fetchConnectors/fulfilled when fetching connectors', async () => {
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connectors`,
-            connectorsServerPayload,
-            { query: { search: '' } }
-          );
-          await store.dispatch(fetchConnectors({ clusterName }));
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnectors.pending.type },
-            {
-              type: fetchConnectors.fulfilled.type,
-              payload: { connectors },
-            },
-          ]);
-        });
-        it('creates fetchConnectors/rejected', async () => {
-          fetchMock.getOnce(`/api/clusters/${clusterName}/connectors`, 404, {
-            query: { search: '' },
-          });
-          await store.dispatch(fetchConnectors({ clusterName }));
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnectors.pending.type },
-            {
-              type: fetchConnectors.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connectors?search=`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-      describe('fetchConnector', () => {
-        it('creates fetchConnector/fulfilled when fetching connector', async () => {
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
-            connectorServerPayload
-          );
-          await store.dispatch(
-            fetchConnector({ clusterName, connectName, connectorName })
-          );
-
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnector.pending.type },
-            {
-              type: fetchConnector.fulfilled.type,
-              payload: { connector },
-            },
-          ]);
-        });
-        it('creates fetchConnector/rejected', async () => {
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
-            404
-          );
-          await store.dispatch(
-            fetchConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnector.pending.type },
-            {
-              type: fetchConnector.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-      describe('createConnector', () => {
-        it('creates createConnector/fulfilled when fetching connects', async () => {
-          fetchMock.postOnce(
-            {
-              url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`,
-              body: {
-                name: connectorName,
-                config: connector.config,
-              },
-            },
-            connectorServerPayload
-          );
-          await store.dispatch(
-            createConnector({
-              clusterName,
-              connectName,
-              newConnector: {
-                name: connectorName,
-                config: connector.config,
-              },
-            })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: createConnector.pending.type },
-            {
-              type: createConnector.fulfilled.type,
-              payload: { connector },
-            },
-          ]);
-        });
-        it('creates createConnector/rejected', async () => {
-          fetchMock.postOnce(
-            {
-              url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`,
-              body: {
-                name: connectorName,
-                config: connector.config,
-              },
-            },
-            404
-          );
-          await store.dispatch(
-            createConnector({
-              clusterName,
-              connectName,
-              newConnector: {
-                name: connectorName,
-                config: connector.config,
-              },
-            })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: createConnector.pending.type },
-            {
-              type: createConnector.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-      describe('deleteConnector', () => {
-        it('creates deleteConnector/fulfilled', async () => {
-          fetchMock.deleteOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
-            {}
-          );
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connectors?search=`,
-            connectorsServerPayload
-          );
-          await store.dispatch(
-            deleteConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: deleteConnector.pending.type },
-            { type: fetchConnectors.pending.type },
-            {
-              type: deleteConnector.fulfilled.type,
-              payload: { connectorName },
-            },
-          ]);
-        });
-        it('creates deleteConnector/rejected', async () => {
-          fetchMock.deleteOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
-            404
-          );
-          try {
-            await store.dispatch(
-              deleteConnector({ clusterName, connectName, connectorName })
-            );
-          } catch {
-            expect(getTypeAndPayload(store)).toEqual([
-              { type: deleteConnector.pending.type },
-              {
-                type: deleteConnector.rejected.type,
-                payload: {
-                  alert: {
-                    subject: 'local-first-hdfs-source-connector',
-                    title: 'Kafka Connect Connector Delete',
-                    response: {
-                      status: 404,
-                      statusText: 'Not Found',
-                      url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
-                    },
-                  },
-                },
-              },
-            ]);
-          }
-        });
-      });
-      describe('fetchConnectorTasks', () => {
-        it('creates fetchConnectorTasks/fulfilled when fetching connects', async () => {
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
-            tasksServerPayload
-          );
-          await store.dispatch(
-            fetchConnectorTasks({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnectorTasks.pending.type },
-            {
-              type: fetchConnectorTasks.fulfilled.type,
-              payload: { tasks },
-            },
-          ]);
-        });
-        it('creates fetchConnectorTasks/rejected', async () => {
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
-            404
-          );
-          await store.dispatch(
-            fetchConnectorTasks({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnectorTasks.pending.type },
-            {
-              type: fetchConnectorTasks.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-      describe('restartConnector', () => {
-        it('creates restartConnector/fulfilled', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`,
-            { message: 'success' }
-          );
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
-            tasksServerPayload
-          );
-          await store.dispatch(
-            restartConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: restartConnector.pending.type },
-            { type: fetchConnectorTasks.pending.type },
-            { type: restartConnector.fulfilled.type },
-          ]);
-        });
-        it('creates restartConnector/rejected', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`,
-            404
-          );
-          await store.dispatch(
-            restartConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: restartConnector.pending.type },
-            {
-              type: restartConnector.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-      describe('pauseConnector', () => {
-        it('creates pauseConnector/fulfilled when fetching connects', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`,
-            { message: 'success' }
-          );
-          await store.dispatch(
-            pauseConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: pauseConnector.pending.type },
-            {
-              type: setConnectorStatusState.type,
-              payload: {
-                connectorState: ConnectorState.PAUSED,
-                taskState: ConnectorTaskStatus.PAUSED,
-              },
-            },
-            { type: pauseConnector.fulfilled.type },
-          ]);
-        });
-        it('creates pauseConnector/rejected', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`,
-            404
-          );
-          await store.dispatch(
-            pauseConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: pauseConnector.pending.type },
-            {
-              type: pauseConnector.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`,
-              },
-            },
-          ]);
-        });
-      });
-      describe('resumeConnector', () => {
-        it('creates resumeConnector/fulfilled when fetching connects', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`,
-            { message: 'success' }
-          );
-          await store.dispatch(
-            resumeConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: resumeConnector.pending.type },
-            {
-              type: setConnectorStatusState.type,
-              payload: {
-                connectorState: ConnectorState.RUNNING,
-                taskState: ConnectorTaskStatus.RUNNING,
-              },
-            },
-            { type: resumeConnector.fulfilled.type },
-          ]);
-        });
-        it('creates resumeConnector/rejected', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`,
-            404
-          );
-          await store.dispatch(
-            resumeConnector({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: resumeConnector.pending.type },
-            {
-              type: resumeConnector.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`,
-              },
-            },
-          ]);
-        });
-      });
-      describe('restartConnectorTask', () => {
-        it('creates restartConnectorTask/fulfilled when fetching connects', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`,
-            { message: 'success' }
-          );
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
-            tasksServerPayload
-          );
-          await store.dispatch(
-            restartConnectorTask({
-              clusterName,
-              connectName,
-              connectorName,
-              taskId,
-            })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: restartConnectorTask.pending.type },
-            { type: fetchConnectorTasks.pending.type },
-            {
-              type: fetchConnectorTasks.fulfilled.type,
-              payload: { tasks },
-            },
-            ...getAlertActions(store),
-            { type: restartConnectorTask.fulfilled.type },
-          ]);
-        });
-        it('creates restartConnectorTask/rejected', async () => {
-          fetchMock.postOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`,
-            404
-          );
-          await store.dispatch(
-            restartConnectorTask({
-              clusterName,
-              connectName,
-              connectorName,
-              taskId,
-            })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: restartConnectorTask.pending.type },
-            {
-              type: restartConnectorTask.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`,
-              },
-            },
-          ]);
-        });
-      });
-      describe('fetchConnectorConfig', () => {
-        it('creates fetchConnectorConfig/fulfilled when fetching connects', async () => {
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
-            connector.config
-          );
-          await store.dispatch(
-            fetchConnectorConfig({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnectorConfig.pending.type },
-            {
-              type: fetchConnectorConfig.fulfilled.type,
-              payload: { config: connector.config },
-            },
-          ]);
-        });
-        it('creates fetchConnectorConfig/rejected', async () => {
-          fetchMock.getOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
-            404
-          );
-          await store.dispatch(
-            fetchConnectorConfig({ clusterName, connectName, connectorName })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: fetchConnectorConfig.pending.type },
-            {
-              type: fetchConnectorConfig.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-      describe('updateConnectorConfig', () => {
-        it('creates updateConnectorConfig/fulfilled when fetching connects', async () => {
-          fetchMock.putOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
-            connectorServerPayload
-          );
-
-          await store.dispatch(
-            updateConnectorConfig({
-              clusterName,
-              connectName,
-              connectorName,
-              connectorConfig: connector.config,
-            })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: updateConnectorConfig.pending.type, payload: undefined },
-            { type: fetchConnector.pending.type },
-            ...getAlertActions(store),
-            {
-              type: updateConnectorConfig.fulfilled.type,
-              payload: { connector },
-            },
-          ]);
-        });
-        it('creates updateConnectorConfig/rejected', async () => {
-          fetchMock.putOnce(
-            `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
-            404
-          );
-          await store.dispatch(
-            updateConnectorConfig({
-              clusterName,
-              connectName,
-              connectorName,
-              connectorConfig: connector.config,
-            })
-          );
-          expect(getTypeAndPayload(store)).toEqual([
-            { type: updateConnectorConfig.pending.type },
-            {
-              type: updateConnectorConfig.rejected.type,
-              payload: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
-                message: undefined,
-              },
-            },
-          ]);
-        });
-      });
-    });
-  });
-});

+ 0 - 132
kafka-ui-react-app/src/redux/reducers/connect/__test__/selectors.spec.ts

@@ -1,132 +0,0 @@
-import {
-  fetchConnector,
-  fetchConnectorConfig,
-  fetchConnectors,
-  fetchConnectorTasks,
-  fetchConnects,
-} from 'redux/reducers/connect/connectSlice';
-import { store } from 'redux/store';
-import * as selectors from 'redux/reducers/connect/selectors';
-
-import { connects, connectors, connector, tasks } from './fixtures';
-
-describe('Connect selectors', () => {
-  describe('Initial State', () => {
-    it('returns initial values', () => {
-      expect(selectors.getAreConnectsFetching(store.getState())).toEqual(false);
-      expect(selectors.getConnects(store.getState())).toEqual([]);
-      expect(selectors.getAreConnectorsFetching(store.getState())).toEqual(
-        false
-      );
-      expect(selectors.getConnectors(store.getState())).toEqual([]);
-      expect(selectors.getFailedConnectors(store.getState())).toEqual([]);
-      expect(selectors.getFailedTasks(store.getState())).toEqual(0);
-      expect(selectors.getIsConnectorFetching(store.getState())).toEqual(false);
-      expect(selectors.getConnector(store.getState())).toEqual(null);
-      expect(selectors.getConnectorStatus(store.getState())).toEqual(undefined);
-      expect(selectors.getIsConnectorDeleting(store.getState())).toEqual(false);
-      expect(selectors.getIsConnectorRestarting(store.getState())).toEqual(
-        false
-      );
-      expect(selectors.getIsConnectorPausing(store.getState())).toEqual(false);
-      expect(selectors.getIsConnectorResuming(store.getState())).toEqual(false);
-      expect(selectors.getIsConnectorActionRunning(store.getState())).toEqual(
-        false
-      );
-      expect(selectors.getAreConnectorTasksFetching(store.getState())).toEqual(
-        false
-      );
-      expect(selectors.getConnectorTasks(store.getState())).toEqual([]);
-      expect(selectors.getConnectorRunningTasksCount(store.getState())).toEqual(
-        0
-      );
-      expect(selectors.getConnectorFailedTasksCount(store.getState())).toEqual(
-        0
-      );
-      expect(selectors.getIsConnectorConfigFetching(store.getState())).toEqual(
-        false
-      );
-      expect(selectors.getConnectorConfig(store.getState())).toEqual(null);
-    });
-  });
-
-  describe('state', () => {
-    it('returns connects', () => {
-      store.dispatch({
-        type: fetchConnects.fulfilled.type,
-        payload: { connects },
-      });
-      expect(selectors.getConnects(store.getState())).toEqual(connects);
-    });
-
-    it('returns connectors', () => {
-      store.dispatch({
-        type: fetchConnectors.fulfilled.type,
-        payload: { connectors },
-      });
-      expect(selectors.getConnectors(store.getState())).toEqual(connectors);
-    });
-
-    it('returns failed connectors', () => {
-      store.dispatch({
-        type: fetchConnectors.fulfilled.type,
-        payload: { connectors },
-      });
-      expect(selectors.getFailedConnectors(store.getState()).length).toEqual(1);
-    });
-
-    it('returns failed tasks', () => {
-      store.dispatch({
-        type: fetchConnectors.fulfilled.type,
-        payload: { connectors },
-      });
-      expect(selectors.getFailedTasks(store.getState())).toEqual(1);
-    });
-
-    it('returns sorted topics', () => {
-      store.dispatch({
-        type: fetchConnectors.fulfilled.type,
-        payload: { connectors },
-      });
-      const sortedTopics = selectors.getSortedTopics(store.getState());
-      if (sortedTopics[0] && sortedTopics[0].length > 1) {
-        expect(sortedTopics[0]).toEqual(['a', 'b', 'c']);
-      }
-    });
-
-    it('returns connector', () => {
-      store.dispatch({
-        type: fetchConnector.fulfilled.type,
-        payload: { connector },
-      });
-      expect(selectors.getConnector(store.getState())).toEqual(connector);
-      expect(selectors.getConnectorStatus(store.getState())).toEqual(
-        connector.status.state
-      );
-    });
-
-    it('returns connector tasks', () => {
-      store.dispatch({
-        type: fetchConnectorTasks.fulfilled.type,
-        payload: { tasks },
-      });
-      expect(selectors.getConnectorTasks(store.getState())).toEqual(tasks);
-      expect(selectors.getConnectorRunningTasksCount(store.getState())).toEqual(
-        2
-      );
-      expect(selectors.getConnectorFailedTasksCount(store.getState())).toEqual(
-        1
-      );
-    });
-
-    it('returns connector config', () => {
-      store.dispatch({
-        type: fetchConnectorConfig.fulfilled.type,
-        payload: { config: connector.config },
-      });
-      expect(selectors.getConnectorConfig(store.getState())).toEqual(
-        connector.config
-      );
-    });
-  });
-});

+ 0 - 478
kafka-ui-react-app/src/redux/reducers/connect/connectSlice.ts

@@ -1,478 +0,0 @@
-import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
-import {
-  Connect,
-  Connector,
-  ConnectorAction,
-  ConnectorState,
-  ConnectorTaskStatus,
-  FullConnectorInfo,
-  NewConnector,
-  Task,
-  TaskId,
-} from 'generated-sources';
-import { kafkaConnectApiClient } from 'lib/api';
-import { getResponse } from 'lib/errorHandling';
-import {
-  ClusterName,
-  ConnectName,
-  ConnectorConfig,
-  ConnectorName,
-  ConnectorSearch,
-  ConnectState,
-} from 'redux/interfaces';
-import { showSuccessAlert } from 'redux/reducers/alerts/alertsSlice';
-
-export const fetchConnects = createAsyncThunk<
-  { connects: Connect[] },
-  ClusterName
->('connect/fetchConnects', async (clusterName, { rejectWithValue }) => {
-  try {
-    const connects = await kafkaConnectApiClient.getConnects({ clusterName });
-
-    return { connects };
-  } catch (err) {
-    return rejectWithValue(await getResponse(err as Response));
-  }
-});
-
-export const fetchConnectors = createAsyncThunk<
-  { connectors: FullConnectorInfo[] },
-  { clusterName: ClusterName; search?: string }
->(
-  'connect/fetchConnectors',
-  async ({ clusterName, search = '' }, { rejectWithValue }) => {
-    try {
-      const connectors = await kafkaConnectApiClient.getAllConnectors({
-        clusterName,
-        search,
-      });
-
-      return { connectors };
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const fetchConnector = createAsyncThunk<
-  { connector: Connector },
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }
->(
-  'connect/fetchConnector',
-  async ({ clusterName, connectName, connectorName }, { rejectWithValue }) => {
-    try {
-      const connector = await kafkaConnectApiClient.getConnector({
-        clusterName,
-        connectName,
-        connectorName,
-      });
-
-      return { connector };
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const createConnector = createAsyncThunk<
-  { connector: Connector },
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    newConnector: NewConnector;
-  }
->(
-  'connect/createConnector',
-  async ({ clusterName, connectName, newConnector }, { rejectWithValue }) => {
-    try {
-      const connector = await kafkaConnectApiClient.createConnector({
-        clusterName,
-        connectName,
-        newConnector,
-      });
-
-      return { connector };
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const deleteConnector = createAsyncThunk<
-  { connectorName: string },
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }
->(
-  'connect/deleteConnector',
-  async (
-    { clusterName, connectName, connectorName },
-    { rejectWithValue, dispatch }
-  ) => {
-    try {
-      await kafkaConnectApiClient.deleteConnector({
-        clusterName,
-        connectName,
-        connectorName,
-      });
-
-      dispatch(fetchConnectors({ clusterName, search: '' }));
-
-      return { connectorName };
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const fetchConnectorTasks = createAsyncThunk<
-  { tasks: Task[] },
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }
->(
-  'connect/fetchConnectorTasks',
-  async ({ clusterName, connectName, connectorName }, { rejectWithValue }) => {
-    try {
-      const tasks = await kafkaConnectApiClient.getConnectorTasks({
-        clusterName,
-        connectName,
-        connectorName,
-      });
-
-      return { tasks };
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const restartConnector = createAsyncThunk<
-  undefined,
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }
->(
-  'connect/restartConnector',
-  async (
-    { clusterName, connectName, connectorName },
-    { rejectWithValue, dispatch }
-  ) => {
-    try {
-      await kafkaConnectApiClient.updateConnectorState({
-        clusterName,
-        connectName,
-        connectorName,
-        action: ConnectorAction.RESTART,
-      });
-
-      dispatch(
-        fetchConnectorTasks({
-          clusterName,
-          connectName,
-          connectorName,
-        })
-      );
-
-      return undefined;
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const restartTasks = createAsyncThunk<
-  undefined,
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-    action: ConnectorAction;
-  }
->(
-  'connect/restartTasks',
-  async (
-    { clusterName, connectName, connectorName, action },
-    { rejectWithValue, dispatch }
-  ) => {
-    try {
-      await kafkaConnectApiClient.updateConnectorState({
-        clusterName,
-        connectName,
-        connectorName,
-        action,
-      });
-
-      dispatch(
-        fetchConnectorTasks({
-          clusterName,
-          connectName,
-          connectorName,
-        })
-      );
-
-      return undefined;
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const restartConnectorTask = createAsyncThunk<
-  undefined,
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-    taskId: TaskId['task'];
-  }
->(
-  'connect/restartConnectorTask',
-  async (
-    { clusterName, connectName, connectorName, taskId },
-    { rejectWithValue, dispatch }
-  ) => {
-    try {
-      await kafkaConnectApiClient.restartConnectorTask({
-        clusterName,
-        connectName,
-        connectorName,
-        taskId: Number(taskId),
-      });
-
-      await dispatch(
-        fetchConnectorTasks({
-          clusterName,
-          connectName,
-          connectorName,
-        })
-      );
-
-      dispatch(
-        showSuccessAlert({
-          id: `connect-${connectName}-${clusterName}`,
-          message: 'Tasks successfully restarted.',
-        })
-      );
-
-      return undefined;
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const fetchConnectorConfig = createAsyncThunk<
-  { config: { [key: string]: unknown } },
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }
->(
-  'connect/fetchConnectorConfig',
-  async ({ clusterName, connectName, connectorName }, { rejectWithValue }) => {
-    try {
-      const config = await kafkaConnectApiClient.getConnectorConfig({
-        clusterName,
-        connectName,
-        connectorName,
-      });
-
-      return { config };
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const updateConnectorConfig = createAsyncThunk<
-  { connector: Connector },
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-    connectorConfig: ConnectorConfig;
-  }
->(
-  'connect/updateConnectorConfig',
-  async (
-    { clusterName, connectName, connectorName, connectorConfig },
-    { rejectWithValue, dispatch }
-  ) => {
-    try {
-      const connector = await kafkaConnectApiClient.setConnectorConfig({
-        clusterName,
-        connectName,
-        connectorName,
-        requestBody: connectorConfig,
-      });
-      dispatch(fetchConnector({ clusterName, connectName, connectorName }));
-      dispatch(
-        showSuccessAlert({
-          id: `connector-${connectorName}-${clusterName}`,
-          message: 'Connector config updated.',
-        })
-      );
-
-      return { connector };
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const initialState: ConnectState = {
-  connects: [],
-  connectors: [],
-  currentConnector: {
-    connector: null,
-    tasks: [],
-    config: null,
-  },
-  search: '',
-};
-
-const connectSlice = createSlice({
-  name: 'connect',
-  initialState,
-  reducers: {
-    setConnectorStatusState: (state, { payload }) => {
-      const { connector, tasks } = state.currentConnector;
-
-      if (connector) {
-        connector.status.state = payload.connectorState;
-      }
-
-      state.currentConnector.tasks = tasks.map((task) => ({
-        ...task,
-        status: {
-          ...task.status,
-          state: payload.taskState,
-        },
-      }));
-    },
-  },
-  extraReducers: (builder) => {
-    builder.addCase(fetchConnects.fulfilled, (state, { payload }) => {
-      state.connects = payload.connects;
-    });
-    builder.addCase(fetchConnectors.fulfilled, (state, { payload }) => {
-      state.connectors = payload.connectors;
-    });
-    builder.addCase(fetchConnector.fulfilled, (state, { payload }) => {
-      state.currentConnector.connector = payload.connector;
-    });
-    builder.addCase(createConnector.fulfilled, (state, { payload }) => {
-      state.currentConnector.connector = payload.connector;
-    });
-    builder.addCase(deleteConnector.fulfilled, (state, { payload }) => {
-      state.connectors = state.connectors.filter(
-        ({ name }) => name !== payload.connectorName
-      );
-    });
-    builder.addCase(fetchConnectorTasks.fulfilled, (state, { payload }) => {
-      state.currentConnector.tasks = payload.tasks;
-    });
-    builder.addCase(fetchConnectorConfig.fulfilled, (state, { payload }) => {
-      state.currentConnector.config = payload.config;
-    });
-    builder.addCase(updateConnectorConfig.fulfilled, (state, { payload }) => {
-      state.currentConnector.connector = payload.connector;
-      state.currentConnector.config = payload.connector.config;
-    });
-  },
-});
-
-export const { setConnectorStatusState } = connectSlice.actions;
-
-export const pauseCurrentConnector = () =>
-  setConnectorStatusState({
-    connectorState: ConnectorState.PAUSED,
-    taskState: ConnectorTaskStatus.PAUSED,
-  });
-
-export const resumeCurrentConnector = () =>
-  setConnectorStatusState({
-    connectorState: ConnectorState.RUNNING,
-    taskState: ConnectorTaskStatus.RUNNING,
-  });
-
-export const pauseConnector = createAsyncThunk<
-  undefined,
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }
->(
-  'connect/pauseConnector',
-  async (
-    { clusterName, connectName, connectorName },
-    { rejectWithValue, dispatch }
-  ) => {
-    try {
-      await kafkaConnectApiClient.updateConnectorState({
-        clusterName,
-        connectName,
-        connectorName,
-        action: ConnectorAction.PAUSE,
-      });
-
-      dispatch(pauseCurrentConnector());
-
-      return undefined;
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const resumeConnector = createAsyncThunk<
-  undefined,
-  {
-    clusterName: ClusterName;
-    connectName: ConnectName;
-    connectorName: ConnectorName;
-  }
->(
-  'connect/resumeConnector',
-  async (
-    { clusterName, connectName, connectorName },
-    { rejectWithValue, dispatch }
-  ) => {
-    try {
-      await kafkaConnectApiClient.updateConnectorState({
-        clusterName,
-        connectName,
-        connectorName,
-        action: ConnectorAction.RESUME,
-      });
-
-      dispatch(resumeCurrentConnector());
-
-      return undefined;
-    } catch (err) {
-      return rejectWithValue(await getResponse(err as Response));
-    }
-  }
-);
-
-export const setConnectorSearch = (connectorSearch: ConnectorSearch) => {
-  return fetchConnectors({
-    clusterName: connectorSearch.clusterName,
-    search: connectorSearch.search,
-  });
-};
-
-export default connectSlice.reducer;

+ 0 - 177
kafka-ui-react-app/src/redux/reducers/connect/selectors.ts

@@ -1,177 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-import { ConnectState, RootState } from 'redux/interfaces';
-import { createFetchingSelector } from 'redux/reducers/loader/selectors';
-import {
-  ConnectorTaskStatus,
-  ConnectorState,
-  FullConnectorInfo,
-} from 'generated-sources';
-import sortBy from 'lodash/sortBy';
-import { AsyncRequestStatus } from 'lib/constants';
-
-import {
-  deleteConnector,
-  fetchConnector,
-  fetchConnectorConfig,
-  fetchConnectors,
-  fetchConnectorTasks,
-  fetchConnects,
-  pauseConnector,
-  restartConnector,
-  resumeConnector,
-} from './connectSlice';
-
-const connectState = ({ connect }: RootState): ConnectState => connect;
-
-const getConnectsFetchingStatus = createFetchingSelector(
-  fetchConnects.typePrefix
-);
-export const getAreConnectsFetching = createSelector(
-  getConnectsFetchingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-export const getConnects = createSelector(
-  connectState,
-  ({ connects }) => connects
-);
-
-const getConnectorsFetchingStatus = createFetchingSelector(
-  fetchConnectors.typePrefix
-);
-export const getAreConnectorsFetching = createSelector(
-  getConnectorsFetchingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-export const getConnectors = createSelector(
-  connectState,
-  ({ connectors }) => connectors
-);
-
-export const getFailedConnectors = createSelector(
-  connectState,
-  ({ connectors }) => {
-    return connectors.filter(
-      (connector: FullConnectorInfo) =>
-        connector.status.state === ConnectorState.FAILED
-    );
-  }
-);
-
-export const getFailedTasks = createSelector(connectState, ({ connectors }) => {
-  return connectors
-    .map((connector: FullConnectorInfo) => connector.failedTasksCount || 0)
-    .reduce((acc: number, value: number) => acc + value, 0);
-});
-
-export const getSortedTopics = createSelector(connectState, ({ connectors }) =>
-  connectors.map(({ topics }) => sortBy(topics || []))
-);
-
-const getConnectorFetchingStatus = createFetchingSelector(
-  fetchConnector.typePrefix
-);
-export const getIsConnectorFetching = createSelector(
-  getConnectorFetchingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-const getCurrentConnector = createSelector(
-  connectState,
-  ({ currentConnector }) => currentConnector
-);
-
-export const getConnector = createSelector(
-  getCurrentConnector,
-  ({ connector }) => connector
-);
-
-export const getConnectorStatus = createSelector(
-  getConnector,
-  (connector) => connector?.status?.state
-);
-
-const getConnectorDeletingStatus = createFetchingSelector(
-  deleteConnector.typePrefix
-);
-export const getIsConnectorDeleting = createSelector(
-  getConnectorDeletingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-const getConnectorRestartingStatus = createFetchingSelector(
-  restartConnector.typePrefix
-);
-export const getIsConnectorRestarting = createSelector(
-  getConnectorRestartingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-const getConnectorPausingStatus = createFetchingSelector(
-  pauseConnector.typePrefix
-);
-export const getIsConnectorPausing = createSelector(
-  getConnectorPausingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-const getConnectorResumingStatus = createFetchingSelector(
-  resumeConnector.typePrefix
-);
-export const getIsConnectorResuming = createSelector(
-  getConnectorResumingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-export const getIsConnectorActionRunning = createSelector(
-  getIsConnectorRestarting,
-  getIsConnectorPausing,
-  getIsConnectorResuming,
-  (restarting, pausing, resuming) => restarting || pausing || resuming
-);
-
-const getConnectorTasksFetchingStatus = createFetchingSelector(
-  fetchConnectorTasks.typePrefix
-);
-export const getAreConnectorTasksFetching = createSelector(
-  getConnectorTasksFetchingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-export const getConnectorTasks = createSelector(
-  getCurrentConnector,
-  ({ tasks }) => tasks
-);
-
-export const getConnectorRunningTasksCount = createSelector(
-  getConnectorTasks,
-  (tasks) =>
-    tasks.filter((task) => task.status?.state === ConnectorTaskStatus.RUNNING)
-      .length
-);
-
-export const getConnectorFailedTasksCount = createSelector(
-  getConnectorTasks,
-  (tasks) =>
-    tasks.filter((task) => task.status?.state === ConnectorTaskStatus.FAILED)
-      .length
-);
-
-const getConnectorConfigFetchingStatus = createFetchingSelector(
-  fetchConnectorConfig.typePrefix
-);
-export const getIsConnectorConfigFetching = createSelector(
-  getConnectorConfigFetchingStatus,
-  (status) => status === AsyncRequestStatus.pending
-);
-
-export const getConnectorConfig = createSelector(
-  getCurrentConnector,
-  ({ config }) => config
-);
-
-export const getConnectorSearch = createSelector(
-  connectState,
-  (state) => state.search
-);

+ 0 - 2
kafka-ui-react-app/src/redux/reducers/index.ts

@@ -2,7 +2,6 @@ import { combineReducers } from '@reduxjs/toolkit';
 import loader from 'redux/reducers/loader/loaderSlice';
 import alerts from 'redux/reducers/alerts/alertsSlice';
 import schemas from 'redux/reducers/schemas/schemasSlice';
-import connect from 'redux/reducers/connect/connectSlice';
 import topicMessages from 'redux/reducers/topicMessages/topicMessagesSlice';
 import topics from 'redux/reducers/topics/topicsSlice';
 import consumerGroups from 'redux/reducers/consumerGroups/consumerGroupsSlice';
@@ -15,6 +14,5 @@ export default combineReducers({
   topicMessages,
   consumerGroups,
   schemas,
-  connect,
   ksqlDb,
 });

+ 1 - 2
kafka-ui-react-app/src/theme/theme.ts

@@ -1,5 +1,4 @@
-/* eslint-disable import/prefer-default-export */
-export const Colors = {
+const Colors = {
   neutral: {
     '0': '#FFFFFF',
     '3': '#f9fafa',