Browse Source

[ISSUE-1189] Refactor Schemas store (#1327)

* [ISSUE-1189] Refactor Schemas store

* Design review fix (#1341)

* Design review fix init

* changed pagination tests, refactored styles, fixed topicform

* redesign fix code cleanup

* styled select position

* styled select fix bracket

* resolved code review

* moved latest version styles to theme

* fixed queryByRole in pagination tests

Co-authored-by: Ekaterina Petrova <epetrova@provectus.com>
Co-authored-by: Oleg Shuralev <workshur@gmail.com>

* Inconsistency in updating the global compatibility level (drop down menu) (#1363)

* Fix select in schema registry

* refactored GlobalSchemaSelector & ListItem

* Moved list & list item tests to react-testing-library

* Added some tests for GlobalSchemaSelector

* Added props test

* Specs

Co-authored-by: Ekaterina Petrova <epetrova@provectus.com>
Co-authored-by: Oleg Shuralev <workshur@gmail.com>

* Feedback

* Cleanup

Co-authored-by: Ekaterina Petrova <32833172+Hurenka@users.noreply.github.com>
Co-authored-by: Ekaterina Petrova <epetrova@provectus.com>
Oleg Shur 3 years ago
parent
commit
613348faa2
85 changed files with 1398 additions and 4181 deletions
  1. 1 1
      kafka-ui-react-app/src/components/Alerts/__tests__/Alerts.spec.tsx
  2. 22 0
      kafka-ui-react-app/src/components/App.styled.ts
  3. 7 10
      kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx
  4. 5 6
      kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx
  5. 12 0
      kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/__snapshots__/Actions.spec.tsx.snap
  6. 2 0
      kafka-ui-react-app/src/components/Connect/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap
  7. 22 2
      kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap
  8. 2 0
      kafka-ui-react-app/src/components/Connect/New/__tests__/__snapshots__/New.spec.tsx.snap
  9. 14 17
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/__test__/ResetOffsets.spec.tsx
  10. 1 8
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx
  11. 5 0
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts
  12. 5 3
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx
  13. 5 8
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx
  14. 15 30
      kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx
  15. 7 15
      kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenuItem.spec.tsx
  16. 5 12
      kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx
  17. 93 92
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  18. 0 49
      kafka-ui-react-app/src/components/Schemas/Details/DetailsContainer.ts
  19. 7 6
      kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.styled.ts
  20. 94 118
      kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx
  21. 13 9
      kafka-ui-react-app/src/components/Schemas/Edit/Edit.styled.ts
  22. 113 126
      kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx
  23. 0 38
      kafka-ui-react-app/src/components/Schemas/Edit/EditContainer.ts
  24. 39 66
      kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx
  25. 0 441
      kafka-ui-react-app/src/components/Schemas/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap
  26. 1 1
      kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.styled.ts
  27. 89 56
      kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx
  28. 31 74
      kafka-ui-react-app/src/components/Schemas/List/List.tsx
  29. 0 31
      kafka-ui-react-app/src/components/Schemas/List/ListContainer.tsx
  30. 6 6
      kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx
  31. 51 103
      kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx
  32. 10 14
      kafka-ui-react-app/src/components/Schemas/List/__test__/ListItem.spec.tsx
  33. 0 90
      kafka-ui-react-app/src/components/Schemas/List/__test__/__snapshots__/ListItem.spec.tsx.snap
  34. 20 0
      kafka-ui-react-app/src/components/Schemas/New/New.styled.ts
  35. 20 36
      kafka-ui-react-app/src/components/Schemas/New/New.tsx
  36. 0 10
      kafka-ui-react-app/src/components/Schemas/New/NewContainer.ts
  37. 18 34
      kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx
  38. 0 1137
      kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap
  39. 45 29
      kafka-ui-react-app/src/components/Schemas/Schemas.tsx
  40. 47 11
      kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx
  41. 13 16
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.spec.tsx
  42. 2 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/__snapshots__/Filters.spec.tsx.snap
  43. 19 20
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/Details.spec.tsx
  44. 21 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/__snapshots__/Details.spec.tsx.snap
  45. 11 15
      kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/DangerZone.spec.tsx
  46. 1 0
      kafka-ui-react-app/src/components/Topics/Topic/Edit/__tests__/__snapshots__/DangerZone.spec.tsx.snap
  47. 2 1
      kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx
  48. 7 18
      kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx
  49. 16 0
      kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.styled.ts
  50. 3 8
      kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx
  51. 0 1
      kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx
  52. 17 0
      kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.styled.ts
  53. 49 69
      kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx
  54. 50 54
      kafka-ui-react-app/src/components/__tests__/App.spec.tsx
  55. 1 6
      kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx
  56. 1 0
      kafka-ui-react-app/src/components/common/Button/Button.styled.ts
  57. 1 5
      kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx
  58. 9 8
      kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx
  59. 71 39
      kafka-ui-react-app/src/components/common/Pagination/Pagination.styled.ts
  60. 9 14
      kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx
  61. 15 18
      kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx
  62. 47 44
      kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx
  63. 0 13
      kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/PageControl.spec.tsx.snap
  64. 1 0
      kafka-ui-react-app/src/components/common/Select/Select.tsx
  65. 2 0
      kafka-ui-react-app/src/components/common/Select/__tests__/__snapshots__/Select.spec.tsx.snap
  66. 2 2
      kafka-ui-react-app/src/lib/__test__/paths.spec.ts
  67. 1 1
      kafka-ui-react-app/src/lib/paths.ts
  68. 33 12
      kafka-ui-react-app/src/lib/testHelpers.tsx
  69. 0 74
      kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts
  70. 0 268
      kafka-ui-react-app/src/redux/actions/__test__/thunks/schemas.spec.ts
  71. 0 44
      kafka-ui-react-app/src/redux/actions/actions.ts
  72. 0 1
      kafka-ui-react-app/src/redux/actions/thunks/index.ts
  73. 0 184
      kafka-ui-react-app/src/redux/actions/thunks/schemas.ts
  74. 24 11
      kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts
  75. 1 1
      kafka-ui-react-app/src/redux/reducers/index.ts
  76. 0 82
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/__snapshots__/reducer.spec.ts.snap
  77. 31 100
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts
  78. 0 118
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/reducer.spec.ts
  79. 0 89
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/selectors.spec.ts
  80. 0 76
      kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts
  81. 85 0
      kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts
  82. 0 71
      kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts
  83. 1 7
      kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts
  84. 4 0
      kafka-ui-react-app/src/theme/index.scss
  85. 21 1
      kafka-ui-react-app/src/theme/theme.ts

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

@@ -8,7 +8,7 @@ import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/
 import userEvent from '@testing-library/user-event';
 
 describe('Alerts', () => {
-  beforeEach(() => render(<Alerts />));
+  beforeEach(() => render(<Alerts />, { store }));
 
   it('renders alerts', async () => {
     const payload: ServerResponse = {

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

@@ -42,6 +42,28 @@ export const Sidebar = styled.div<{ $visible: boolean }>(
       left: -${theme.layout.navBarWidth};
       z-index: 100;
     }
+
+    &::-webkit-scrollbar {
+      width: 8px;
+    }
+
+    &::-webkit-scrollbar-track {
+      background-color: ${theme.scrollbar.trackColor.normal};
+    }
+
+    &::-webkit-scrollbar-thumb {
+      width: 8px;
+      background-color: ${theme.scrollbar.thumbColor.normal};
+      border-radius: 4px;
+    }
+
+    &:hover::-webkit-scrollbar-thumb {
+      background: ${theme.scrollbar.thumbColor.active};
+    }
+
+    &:hover::-webkit-scrollbar-track {
+      background-color: ${theme.scrollbar.trackColor.active};
+    }
   `
 );
 

+ 7 - 10
kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import Brokers from 'components/Brokers/Brokers';
 import { render } from 'lib/testHelpers';
 import { screen, waitFor } from '@testing-library/dom';
-import { Route, StaticRouter } from 'react-router';
+import { Route } from 'react-router';
 import { clusterBrokersPath } from 'lib/paths';
 import fetchMock from 'fetch-mock';
 import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures';
@@ -13,15 +13,12 @@ describe('Brokers Component', () => {
   const clusterName = 'local';
   const renderComponent = () =>
     render(
-      <StaticRouter
-        location={{
-          pathname: clusterBrokersPath(clusterName),
-        }}
-      >
-        <Route path={clusterBrokersPath(':clusterName')}>
-          <Brokers />
-        </Route>
-      </StaticRouter>
+      <Route path={clusterBrokersPath(':clusterName')}>
+        <Brokers />
+      </Route>,
+      {
+        pathname: clusterBrokersPath(clusterName),
+      }
     );
 
   describe('Brokers', () => {

+ 5 - 6
kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { Route, StaticRouter } from 'react-router-dom';
+import { Route } from 'react-router-dom';
 import { ClusterFeaturesEnum } from 'generated-sources';
 import { store } from 'redux/store';
 import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
@@ -29,11 +29,10 @@ jest.mock('components/KsqlDb/KsqlDb', () => () => <div>KsqlDb</div>);
 describe('Cluster', () => {
   const renderComponent = (pathname: string) =>
     render(
-      <StaticRouter location={{ pathname }}>
-        <Route path="/ui/clusters/:clusterName">
-          <Cluster />
-        </Route>
-      </StaticRouter>
+      <Route path="/ui/clusters/:clusterName">
+        <Cluster />
+      </Route>,
+      { pathname, store }
     );
 
   it('renders Brokers', () => {

+ 12 - 0
kafka-ui-react-app/src/components/Connect/Details/Actions/__tests__/__snapshots__/Actions.spec.tsx.snap

@@ -24,6 +24,7 @@ exports[`Actions view matches snapshot 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -74,6 +75,7 @@ exports[`Actions view matches snapshot 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -215,6 +217,7 @@ exports[`Actions view matches snapshot when deleting connector 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -265,6 +268,7 @@ exports[`Actions view matches snapshot when deleting connector 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -406,6 +410,7 @@ exports[`Actions view matches snapshot when failed 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -456,6 +461,7 @@ exports[`Actions view matches snapshot when failed 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -582,6 +588,7 @@ exports[`Actions view matches snapshot when paused 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -632,6 +639,7 @@ exports[`Actions view matches snapshot when paused 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -773,6 +781,7 @@ exports[`Actions view matches snapshot when running connector action 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -823,6 +832,7 @@ exports[`Actions view matches snapshot when running connector action 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -964,6 +974,7 @@ exports[`Actions view matches snapshot when unassigned 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -1014,6 +1025,7 @@ exports[`Actions view matches snapshot when unassigned 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 

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

@@ -24,6 +24,7 @@ exports[`Edit view matches snapshot 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -115,6 +116,7 @@ exports[`Edit view matches snapshot when config has credentials 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 

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

@@ -37,7 +37,7 @@ exports[`Connectors ListItem matches snapshot 1`] = `
   border-radius: 16px;
   height: 20px;
   line-height: 20px;
-  background-color: #E3E6E8;
+  background-color: #F1F2F3;
   color: #171A1C;
   font-size: 12px;
   display: inline-block;
@@ -133,7 +133,14 @@ exports[`Connectors ListItem matches snapshot 1`] = `
           "warning": "#FFEECC",
         },
       },
+      "headingStyles": Object {
+        "h3": Object {
+          "color": "#73848C",
+          "fontSize": "14px",
+        },
+      },
       "layout": Object {
+        "mainColor": "#F1F2F3",
         "minWidth": "1200px",
         "navBarHeight": "3.25rem",
         "navBarWidth": "201px",
@@ -169,6 +176,7 @@ exports[`Connectors ListItem matches snapshot 1`] = `
         "borderColor": "#4F4FFF",
       },
       "paginationStyles": Object {
+        "backgroundColor": "#FFFFFF",
         "borderColor": Object {
           "active": "#454F54",
           "disabled": "#C7CED1",
@@ -181,7 +189,9 @@ exports[`Connectors ListItem matches snapshot 1`] = `
           "hover": "#171A1C",
           "normal": "#171A1C",
         },
+        "currentPage": "#E3E6E8",
       },
+      "panelColor": "#FFFFFF",
       "primaryTabStyles": Object {
         "borderColor": Object {
           "active": "#4F4FFF",
@@ -194,6 +204,16 @@ exports[`Connectors ListItem matches snapshot 1`] = `
           "normal": "#73848C",
         },
       },
+      "scrollbar": Object {
+        "thumbColor": Object {
+          "active": "#73848C",
+          "normal": "#FFFFFF",
+        },
+        "trackColor": Object {
+          "active": "#F1F2F3",
+          "normal": "#FFFFFF",
+        },
+      },
       "secondaryTabStyles": Object {
         "backgroundColor": Object {
           "active": "#E3E6E8",
@@ -226,7 +246,7 @@ exports[`Connectors ListItem matches snapshot 1`] = `
       },
       "tagStyles": Object {
         "backgroundColor": Object {
-          "gray": "#E3E6E8",
+          "gray": "#F1F2F3",
           "green": "#D6F5E0",
           "red": "#FAD1D1",
           "white": "#E3E6E8",

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

@@ -196,6 +196,7 @@ Array [
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -263,6 +264,7 @@ Array [
           name="connectName"
           onBlur={[Function]}
           onChange={[Function]}
+          role="listbox"
         >
           <option
             value="first"

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

@@ -1,6 +1,6 @@
 import React from 'react';
 import fetchMock from 'fetch-mock';
-import { Route, StaticRouter } from 'react-router';
+import { Route } from 'react-router';
 import { screen, waitFor, fireEvent } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
@@ -13,23 +13,20 @@ const { groupId } = consumerGroupPayload;
 
 const renderComponent = () =>
   render(
-    <StaticRouter
-      location={{
-        pathname: clusterConsumerGroupResetOffsetsPath(
-          clusterName,
-          consumerGroupPayload.groupId
-        ),
-      }}
+    <Route
+      path={clusterConsumerGroupResetOffsetsPath(
+        ':clusterName',
+        ':consumerGroupID'
+      )}
     >
-      <Route
-        path={clusterConsumerGroupResetOffsetsPath(
-          ':clusterName',
-          ':consumerGroupID'
-        )}
-      >
-        <ResetOffsets />
-      </Route>
-    </StaticRouter>
+      <ResetOffsets />
+    </Route>,
+    {
+      pathname: clusterConsumerGroupResetOffsetsPath(
+        clusterName,
+        consumerGroupPayload.groupId
+      ),
+    }
   );
 
 const resetConsumerGroupOffsetsMockCalled = () =>

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

@@ -1,7 +1,6 @@
 import React from 'react';
 import List from 'components/ConsumerGroups/List/List';
 import { screen } from '@testing-library/react';
-import { StaticRouter } from 'react-router';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 import { store } from 'redux/store';
@@ -9,13 +8,7 @@ import { fetchConsumerGroups } from 'redux/reducers/consumerGroups/consumerGroup
 import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures';
 
 describe('List', () => {
-  beforeEach(() =>
-    render(
-      <StaticRouter>
-        <List />
-      </StaticRouter>
-    )
-  );
+  beforeEach(() => render(<List />, { store }));
 
   it('renders empty table', () => {
     expect(screen.getByRole('table')).toBeInTheDocument();

+ 5 - 0
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.styled.ts

@@ -0,0 +1,5 @@
+import styled from 'styled-components';
+
+export const SwitchWrapper = styled.div`
+  padding: 16px;
+`;

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

@@ -11,6 +11,8 @@ import { NavLink } from 'react-router-dom';
 import { clusterTopicsPath } from 'lib/paths';
 import Switch from 'components/common/Switch/Switch';
 
+import * as S from './ClustersWidget.styled';
+
 interface Props {
   clusters: Cluster[];
   onlineClusters: Cluster[];
@@ -62,14 +64,14 @@ const ClustersWidget: React.FC<Props> = ({
           </Metrics.Indicator>
         </Metrics.Section>
       </Metrics.Wrapper>
-      <div className="p-4">
+      <S.SwitchWrapper>
         <Switch
           name="switchRoundedDefault"
           checked={showOfflineOnly}
           onChange={handleSwitch}
         />
-        <span>Only offline clusters</span>
-      </div>
+        <label>Only offline clusters</label>
+      </S.SwitchWrapper>
       {clusterList.map((chunkItem) => (
         <Table key={chunkItem.id} isFullwidth>
           <thead>

+ 5 - 8
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import { StaticRouter } from 'react-router';
 import { screen } from '@testing-library/react';
 import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
 import userEvent from '@testing-library/user-event';
@@ -9,13 +8,11 @@ import { offlineCluster, onlineCluster, clusters } from './fixtures';
 
 const setupComponent = () =>
   render(
-    <StaticRouter>
-      <ClustersWidget
-        clusters={clusters}
-        onlineClusters={[onlineCluster]}
-        offlineClusters={[offlineCluster]}
-      />
-    </StaticRouter>
+    <ClustersWidget
+      clusters={clusters}
+      onlineClusters={[onlineCluster]}
+      offlineClusters={[offlineCluster]}
+    />
   );
 
 describe('ClustersWidget', () => {

+ 15 - 30
kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import { StaticRouter } from 'react-router';
 import { screen } from '@testing-library/react';
 import { Cluster, ClusterFeaturesEnum } from 'generated-sources';
 import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
@@ -9,14 +8,8 @@ import { clusterConnectorsPath, clusterConnectsPath } from 'lib/paths';
 import { render } from 'lib/testHelpers';
 
 describe('ClusterMenu', () => {
-  const setupComponent = (
-    cluster: Cluster,
-    pathname?: string,
-    singleMode?: boolean
-  ) => (
-    <StaticRouter location={{ pathname }} context={{}}>
-      <ClusterMenu cluster={cluster} singleMode={singleMode} />
-    </StaticRouter>
+  const setupComponent = (cluster: Cluster, singleMode?: boolean) => (
+    <ClusterMenu cluster={cluster} singleMode={singleMode} />
   );
 
   it('renders cluster menu with default set of features', () => {
@@ -54,13 +47,9 @@ describe('ClusterMenu', () => {
     expect(screen.getByTitle('KSQL DB')).toBeInTheDocument();
   });
   it('renders open cluster menu', () => {
-    render(
-      setupComponent(
-        onlineClusterPayload,
-        clusterConnectorsPath(onlineClusterPayload.name),
-        true
-      )
-    );
+    render(setupComponent(onlineClusterPayload, true), {
+      pathname: clusterConnectorsPath(onlineClusterPayload.name),
+    });
 
     expect(screen.getAllByRole('menuitem').length).toEqual(4);
     expect(screen.getByText(onlineClusterPayload.name)).toBeInTheDocument();
@@ -70,13 +59,11 @@ describe('ClusterMenu', () => {
   });
   it('makes Kafka Connect link active', () => {
     render(
-      setupComponent(
-        {
-          ...onlineClusterPayload,
-          features: [ClusterFeaturesEnum.KAFKA_CONNECT],
-        },
-        clusterConnectorsPath(onlineClusterPayload.name)
-      )
+      setupComponent({
+        ...onlineClusterPayload,
+        features: [ClusterFeaturesEnum.KAFKA_CONNECT],
+      }),
+      { pathname: clusterConnectorsPath(onlineClusterPayload.name) }
     );
     expect(screen.getAllByRole('menuitem').length).toEqual(1);
     userEvent.click(screen.getByRole('menuitem'));
@@ -87,13 +74,11 @@ describe('ClusterMenu', () => {
   });
   it('makes Kafka Connect link active', () => {
     render(
-      setupComponent(
-        {
-          ...onlineClusterPayload,
-          features: [ClusterFeaturesEnum.KAFKA_CONNECT],
-        },
-        clusterConnectsPath(onlineClusterPayload.name)
-      )
+      setupComponent({
+        ...onlineClusterPayload,
+        features: [ClusterFeaturesEnum.KAFKA_CONNECT],
+      }),
+      { pathname: clusterConnectsPath(onlineClusterPayload.name) }
     );
     expect(screen.getAllByRole('menuitem').length).toEqual(1);
     userEvent.click(screen.getByRole('menuitem'));

+ 7 - 15
kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenuItem.spec.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import { StaticRouter } from 'react-router';
 import ClusterMenuItem, {
   ClusterMenuItemProps,
 } from 'components/Nav/ClusterMenuItem';
@@ -7,15 +6,10 @@ import { screen } from '@testing-library/react';
 import { render } from 'lib/testHelpers';
 
 describe('ClusterMenuItem', () => {
-  const setupComponent = (
-    props: Partial<ClusterMenuItemProps> = {},
-    pathname?: string
-  ) => (
-    <StaticRouter location={{ pathname }} context={{}}>
-      <ul>
-        <ClusterMenuItem to="/test" {...props} />
-      </ul>
-    </StaticRouter>
+  const setupComponent = (props: Partial<ClusterMenuItemProps> = {}) => (
+    <ul>
+      <ClusterMenuItem to="/test" {...props} />
+    </ul>
   );
 
   it('renders component with correct title', () => {
@@ -48,11 +42,9 @@ describe('ClusterMenuItem', () => {
 
   it('renders list item with children', () => {
     render(
-      <StaticRouter location={{}} context={{}}>
-        <ul>
-          <ClusterMenuItem to="/test">Test Text Box</ClusterMenuItem>
-        </ul>
-      </StaticRouter>
+      <ul>
+        <ClusterMenuItem to="/test">Test Text Box</ClusterMenuItem>
+      </ul>
     );
     expect(screen.getByRole('menuitem')).toBeInTheDocument();
     expect(screen.queryByRole('link')).toBeInTheDocument();

+ 5 - 12
kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx

@@ -4,29 +4,22 @@ import {
   onlineClusterPayload,
 } from 'redux/reducers/clusters/__test__/fixtures';
 import Nav from 'components/Nav/Nav';
-import { StaticRouter } from 'react-router';
 import { screen } from '@testing-library/react';
 import { render } from 'lib/testHelpers';
 
 describe('Nav', () => {
   it('renders loader', () => {
-    render(
-      <StaticRouter>
-        <Nav clusters={[]} />
-      </StaticRouter>
-    );
+    render(<Nav clusters={[]} />);
     expect(screen.getAllByRole('menuitem').length).toEqual(1);
     expect(screen.getByText('Dashboard')).toBeInTheDocument();
   });
 
   it('renders ClusterMenu', () => {
     render(
-      <StaticRouter>
-        <Nav
-          clusters={[onlineClusterPayload, offlineClusterPayload]}
-          areClustersFulfilled
-        />
-      </StaticRouter>
+      <Nav
+        clusters={[onlineClusterPayload, offlineClusterPayload]}
+        areClustersFulfilled
+      />
     );
     expect(screen.getAllByRole('menu').length).toEqual(3);
     expect(screen.getAllByRole('menuitem').length).toEqual(3);

+ 93 - 92
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -1,8 +1,6 @@
 import React from 'react';
-import { useHistory } from 'react-router';
-import { SchemaSubject } from 'generated-sources';
-import { ClusterName, SchemaName } from 'redux/interfaces';
-import { clusterSchemasPath, clusterSchemaSchemaEditPath } from 'lib/paths';
+import { useHistory, useParams } from 'react-router';
+import { clusterSchemasPath, clusterSchemaEditPath } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import PageLoader from 'components/common/PageLoader/PageLoader';
@@ -13,114 +11,117 @@ import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import {
+  fetchSchemaVersions,
+  getAreSchemasFulfilled,
+  getAreSchemaVersionsFulfilled,
+  schemasApiClient,
+  selectAllSchemaVersions,
+  selectSchemaById,
+} from 'redux/reducers/schemas/schemasSlice';
+import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
+import { getResponse } from 'lib/errorHandling';
 
 import LatestVersionItem from './LatestVersion/LatestVersionItem';
 import SchemaVersion from './SchemaVersion/SchemaVersion';
 import { OldVersionsTitle } from './SchemaVersion/SchemaVersion.styled';
 
-export interface DetailsProps {
-  subject: SchemaName;
-  schema: SchemaSubject;
-  clusterName: ClusterName;
-  versions: SchemaSubject[];
-  areVersionsFetched: boolean;
-  areSchemasFetched: boolean;
-  fetchSchemaVersions: (
-    clusterName: ClusterName,
-    schemaName: SchemaName
-  ) => void;
-  fetchSchemasByClusterName: (clusterName: ClusterName) => void;
-  deleteSchema: (clusterName: ClusterName, subject: string) => Promise<void>;
-}
-
-const Details: React.FC<DetailsProps> = ({
-  subject,
-  schema,
-  clusterName,
-  fetchSchemaVersions,
-  fetchSchemasByClusterName,
-  deleteSchema,
-  versions,
-  areVersionsFetched,
-  areSchemasFetched,
-}) => {
+const Details: React.FC = () => {
+  const history = useHistory();
+  const dispatch = useAppDispatch();
   const { isReadOnly } = React.useContext(ClusterContext);
+  const { clusterName, subject } =
+    useParams<{ clusterName: string; subject: string }>();
   const [
     isDeleteSchemaConfirmationVisible,
     setDeleteSchemaConfirmationVisible,
   ] = React.useState(false);
 
   React.useEffect(() => {
-    fetchSchemasByClusterName(clusterName);
-    fetchSchemaVersions(clusterName, subject);
-  }, [fetchSchemaVersions, fetchSchemasByClusterName, clusterName]);
+    dispatch(fetchSchemaVersions({ clusterName, subject }));
+  }, []);
 
-  const history = useHistory();
-  const onDelete = React.useCallback(() => {
-    deleteSchema(clusterName, subject);
-    history.push(clusterSchemasPath(clusterName));
-  }, [deleteSchema, clusterName, subject]);
+  const areSchemasFetched = useAppSelector(getAreSchemasFulfilled);
+  const areVersionsFetched = useAppSelector(getAreSchemaVersionsFulfilled);
+  const schema = useAppSelector((state) => selectSchemaById(state, subject));
+  const versions = useAppSelector((state) =>
+    selectAllSchemaVersions(state).filter((v) => v.subject === subject)
+  );
+
+  const onDelete = React.useCallback(async () => {
+    try {
+      await schemasApiClient.deleteSchema({
+        clusterName,
+        subject,
+      });
+      history.push(clusterSchemasPath(clusterName));
+    } catch (e) {
+      const err = await getResponse(e as Response);
+      dispatch(serverErrorAlertAdded(err));
+    }
+  }, [clusterName, subject]);
+
+  if (!areSchemasFetched || !schema) {
+    return <PageLoader />;
+  }
 
   return (
-    <div>
-      {areVersionsFetched && areSchemasFetched ? (
-        <>
-          <div>
-            <PageHeading text={schema.subject}>
-              {!isReadOnly && (
-                <>
-                  <Button
-                    isLink
-                    buttonSize="M"
-                    buttonType="primary"
-                    to={clusterSchemaSchemaEditPath(clusterName, subject)}
-                  >
-                    Edit Schema
-                  </Button>
-                  <Dropdown label={<VerticalElipsisIcon />} right>
-                    <DropdownItem
-                      onClick={() => setDeleteSchemaConfirmationVisible(true)}
-                    >
-                      Remove schema
-                    </DropdownItem>
-                  </Dropdown>
-                  <ConfirmationModal
-                    isOpen={isDeleteSchemaConfirmationVisible}
-                    onCancel={() => setDeleteSchemaConfirmationVisible(false)}
-                    onConfirm={onDelete}
-                  >
-                    Are you sure want to remove <b>{subject}</b> schema?
-                  </ConfirmationModal>
-                </>
-              )}
-            </PageHeading>
-            <LatestVersionItem schema={schema} />
-          </div>
-          <OldVersionsTitle>Old versions</OldVersionsTitle>
-          <Table isFullwidth>
-            <thead>
+    <>
+      <PageHeading text={schema.subject}>
+        {!isReadOnly && (
+          <>
+            <Button
+              isLink
+              buttonSize="M"
+              buttonType="primary"
+              to={clusterSchemaEditPath(clusterName, subject)}
+            >
+              Edit Schema
+            </Button>
+            <Dropdown label={<VerticalElipsisIcon />} right>
+              <DropdownItem
+                onClick={() => setDeleteSchemaConfirmationVisible(true)}
+              >
+                Remove schema
+              </DropdownItem>
+            </Dropdown>
+            <ConfirmationModal
+              isOpen={isDeleteSchemaConfirmationVisible}
+              onCancel={() => setDeleteSchemaConfirmationVisible(false)}
+              onConfirm={onDelete}
+            >
+              Are you sure want to remove <b>{subject}</b> schema?
+            </ConfirmationModal>
+          </>
+        )}
+      </PageHeading>
+      <LatestVersionItem schema={schema} />
+      <OldVersionsTitle>Old versions</OldVersionsTitle>
+      {areVersionsFetched ? (
+        <Table isFullwidth>
+          <thead>
+            <tr>
+              <TableHeaderCell />
+              <TableHeaderCell title="Version" />
+              <TableHeaderCell title="ID" />
+            </tr>
+          </thead>
+          <tbody>
+            {versions.map((version) => (
+              <SchemaVersion key={version.id} version={version} />
+            ))}
+            {versions.length === 0 && (
               <tr>
-                <TableHeaderCell />
-                <TableHeaderCell title="Version" />
-                <TableHeaderCell title="ID" />
+                <td colSpan={10}>No active Schema</td>
               </tr>
-            </thead>
-            <tbody>
-              {versions.map((version) => (
-                <SchemaVersion key={version.id} version={version} />
-              ))}
-              {versions.length === 0 && (
-                <tr>
-                  <td colSpan={10}>No active Schema</td>
-                </tr>
-              )}
-            </tbody>
-          </Table>
-        </>
+            )}
+          </tbody>
+        </Table>
       ) : (
         <PageLoader />
       )}
-    </div>
+    </>
   );
 };
 

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

@@ -1,49 +0,0 @@
-import { connect } from 'react-redux';
-import { ClusterName, RootState } from 'redux/interfaces';
-import { RouteComponentProps, withRouter } from 'react-router-dom';
-import {
-  getIsSchemaVersionFetched,
-  getSchema,
-  getSortedSchemaVersions,
-  getIsSchemaListFetched,
-} from 'redux/reducers/schemas/selectors';
-import {
-  fetchSchemaVersions,
-  deleteSchema,
-  fetchSchemasByClusterName,
-} from 'redux/actions';
-
-import Details from './Details';
-
-interface RouteProps {
-  clusterName: ClusterName;
-  subject: string;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { clusterName, subject },
-    },
-  }: OwnProps
-) => ({
-  subject,
-  schema: getSchema(state, subject),
-  versions: getSortedSchemaVersions(state),
-  areVersionsFetched: getIsSchemaVersionFetched(state),
-  areSchemasFetched: getIsSchemaListFetched(state),
-  clusterName,
-});
-
-const mapDispatchToProps = {
-  fetchSchemaVersions,
-  fetchSchemasByClusterName,
-  deleteSchema,
-};
-
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Details)
-);

+ 7 - 6
kafka-ui-react-app/src/components/Schemas/Details/LatestVersion/LatestVersionItem.styled.ts

@@ -1,9 +1,9 @@
 import styled from 'styled-components';
-import { Colors } from 'theme/theme';
+import theme from 'theme/theme';
 
 export const LatestVersionWrapper = styled.div`
   width: 100%;
-  background-color: ${Colors.neutral[5]};
+  background-color: ${theme.layout.mainColor};
   padding: 16px;
   display: flex;
   justify-content: center;
@@ -12,13 +12,13 @@ export const LatestVersionWrapper = styled.div`
   max-height: 700px;
 
   & > * {
-    background-color: ${Colors.neutral[0]};
+    background-color: ${theme.panelColor};
     padding: 24px;
     overflow-y: scroll;
   }
 
   & > div:first-child {
-    border-radius: 8px 0px 0px 8px;
+    border-radius: 8px 0 0 8px;
     flex-grow: 2;
 
     & > h1 {
@@ -28,7 +28,7 @@ export const LatestVersionWrapper = styled.div`
   }
 
   & > div:last-child {
-    border-radius: 0px 8px 8px 0px;
+    border-radius: 0 8px 8px 0;
     flex-grow: 1;
 
     & > div {
@@ -40,6 +40,7 @@ export const LatestVersionWrapper = styled.div`
 `;
 
 export const MetaDataLabel = styled.h3`
-  color: ${Colors.neutral[50]};
+  color: ${theme.headingStyles.h3.color};
   width: 110px;
+  font-size: ${theme.headingStyles.h3.fontSize};
 `;

+ 94 - 118
kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx

@@ -1,139 +1,115 @@
 import React from 'react';
-import { Provider } from 'react-redux';
-import { mount } from 'enzyme';
-import { store } from 'redux/store';
-import { StaticRouter } from 'react-router';
-import ClusterContext from 'components/contexts/ClusterContext';
-import DetailsContainer from 'components/Schemas/Details/DetailsContainer';
-import Details, { DetailsProps } from 'components/Schemas/Details/Details';
-import { ThemeProvider } from 'styled-components';
-import theme from 'theme/theme';
+import Details from 'components/Schemas/Details/Details';
+import { render } from 'lib/testHelpers';
+import { Route } from 'react-router';
+import { clusterSchemaPath } from 'lib/paths';
+import { screen, waitFor } from '@testing-library/dom';
+import {
+  schemasFulfilledState,
+  schemaVersion,
+} from 'redux/reducers/schemas/__test__/fixtures';
+import fetchMock from 'fetch-mock';
 
-import { jsonSchema, versions } from './fixtures';
-
-const clusterName = 'testCluster';
-const fetchSchemaVersionsMock = jest.fn();
-
-jest.mock(
-  'components/common/ConfirmationModal/ConfirmationModal',
-  () => 'mock-ConfirmationModal'
-);
+const clusterName = 'testClusterName';
+const subject = 'schema7_1';
 
 describe('Details', () => {
-  describe('Container', () => {
-    it('renders view', () => {
-      const wrapper = mount(
-        <ThemeProvider theme={theme}>
-          <Provider store={store}>
-            <StaticRouter>
-              <DetailsContainer />
-            </StaticRouter>
-          </Provider>
-        </ThemeProvider>
+  describe('for an initial state', () => {
+    it('renders pageloader', () => {
+      render(
+        <Route path={clusterSchemaPath(':clusterName', ':subject')}>
+          <Details />
+        </Route>,
+        {
+          pathname: clusterSchemaPath(clusterName, subject),
+          preloadedState: {},
+        }
       );
-
-      expect(wrapper.exists(Details)).toBeTruthy();
+      expect(screen.getByRole('progressbar')).toBeInTheDocument();
+      expect(screen.queryByText(subject)).not.toBeInTheDocument();
+      expect(screen.queryByText('Edit Schema')).not.toBeInTheDocument();
+      expect(screen.queryByText('Remove Schema')).not.toBeInTheDocument();
     });
   });
 
-  describe('View', () => {
-    const setupWrapper = (props: Partial<DetailsProps> = {}) => (
-      <ThemeProvider theme={theme}>
-        <StaticRouter>
-          <Details
-            subject={jsonSchema.subject}
-            schema={jsonSchema}
-            clusterName={clusterName}
-            fetchSchemaVersions={fetchSchemaVersionsMock}
-            deleteSchema={jest.fn()}
-            fetchSchemasByClusterName={jest.fn()}
-            areSchemasFetched
-            areVersionsFetched
-            versions={[]}
-            {...props}
-          />
-        </StaticRouter>
-      </ThemeProvider>
-    );
-    describe('empty table', () => {
-      it('render empty table', () => {
-        const component = mount(setupWrapper());
-        expect(component.find('td').text()).toEqual('No active Schema');
-      });
+  describe('for a loaded scheme', () => {
+    beforeEach(() => {
+      render(
+        <Route path={clusterSchemaPath(':clusterName', ':subject')}>
+          <Details />
+        </Route>,
+        {
+          pathname: clusterSchemaPath(clusterName, subject),
+          preloadedState: {
+            loader: {
+              'schemas/fetch': 'fulfilled',
+            },
+            schemas: schemasFulfilledState,
+          },
+        }
+      );
     });
 
-    describe('Initial state', () => {
-      it('should call fetchSchemaVersions every render', () => {
-        mount(
-          <StaticRouter>
-            {setupWrapper({ fetchSchemaVersions: fetchSchemaVersionsMock })}
-          </StaticRouter>
-        );
-
-        expect(fetchSchemaVersionsMock).toHaveBeenCalledWith(
-          clusterName,
-          jsonSchema.subject
-        );
-      });
+    it('renders component with shema info', () => {
+      expect(screen.getByText('Edit Schema')).toBeInTheDocument();
     });
 
-    describe('when page with schema versions is loading', () => {
-      const wrapper = mount(setupWrapper({ areVersionsFetched: false }));
-
-      it('renders PageLoader', () => {
-        expect(wrapper.exists('PageLoader')).toBeTruthy();
-      });
+    it('renders progressbar for versions block', () => {
+      expect(screen.getByRole('progressbar')).toBeInTheDocument();
+      expect(screen.queryByRole('table')).not.toBeInTheDocument();
     });
+  });
 
-    describe('when page with schema versions loaded', () => {
-      describe('when versions are empty', () => {
-        it('renders table heading without SchemaVersion', () => {
-          const wrapper = mount(setupWrapper());
-          expect(wrapper.exists('LatestVersionItem')).toBeTruthy();
-          expect(wrapper.exists('button')).toBeTruthy();
-          expect(wrapper.exists('thead')).toBeTruthy();
-          expect(wrapper.exists('SchemaVersion')).toBeFalsy();
-        });
-      });
-
-      describe('when schema has versions', () => {
-        it('renders table heading with SchemaVersion', () => {
-          const wrapper = mount(setupWrapper({ versions }));
-          expect(wrapper.exists('LatestVersionItem')).toBeTruthy();
-          expect(wrapper.exists('button')).toBeTruthy();
-          expect(wrapper.exists('thead')).toBeTruthy();
-          expect(wrapper.find('SchemaVersion').length).toEqual(3);
-        });
-      });
+  describe('for a loaded scheme and versions', () => {
+    afterEach(() => fetchMock.restore());
+    it('renders versions table', async () => {
+      const mock = fetchMock.getOnce(
+        `/api/clusters/${clusterName}/schemas/${subject}/versions`,
+        [schemaVersion]
+      );
+      render(
+        <Route path={clusterSchemaPath(':clusterName', ':subject')}>
+          <Details />
+        </Route>,
+        {
+          pathname: clusterSchemaPath(clusterName, subject),
+          preloadedState: {
+            loader: {
+              'schemas/fetch': 'fulfilled',
+            },
+            schemas: schemasFulfilledState,
+          },
+        }
+      );
+      await waitFor(() => expect(mock.called()).toBeTruthy());
 
-      describe('when the readonly flag is set', () => {
-        it('does not render update & delete buttons', () => {
-          expect(
-            mount(
-              <StaticRouter>
-                <ClusterContext.Provider
-                  value={{
-                    isReadOnly: true,
-                    hasKafkaConnectConfigured: true,
-                    hasSchemaRegistryConfigured: true,
-                    isTopicDeletionAllowed: true,
-                  }}
-                >
-                  {setupWrapper({ versions })}
-                </ClusterContext.Provider>
-              </StaticRouter>
-            ).exists('.level-right')
-          ).toBeFalsy();
-        });
-      });
+      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
+      expect(screen.getByRole('table')).toBeInTheDocument();
     });
 
-    describe('when page with schemas are loading', () => {
-      const wrapper = mount(setupWrapper({ areSchemasFetched: false }));
+    it('renders versions table with 0 items', async () => {
+      const mock = fetchMock.getOnce(
+        `/api/clusters/${clusterName}/schemas/${subject}/versions`,
+        []
+      );
+      render(
+        <Route path={clusterSchemaPath(':clusterName', ':subject')}>
+          <Details />
+        </Route>,
+        {
+          pathname: clusterSchemaPath(clusterName, subject),
+          preloadedState: {
+            loader: {
+              'schemas/fetch': 'fulfilled',
+            },
+            schemas: schemasFulfilledState,
+          },
+        }
+      );
+      await waitFor(() => expect(mock.called()).toBeTruthy());
 
-      it('renders PageLoader', () => {
-        expect(wrapper.exists('PageLoader')).toBeTruthy();
-      });
+      expect(screen.getByRole('table')).toBeInTheDocument();
+      expect(screen.getByText('No active Schema')).toBeInTheDocument();
     });
   });
 });

+ 13 - 9
kafka-ui-react-app/src/components/Schemas/Edit/Edit.styled.ts

@@ -30,14 +30,18 @@ export const EditorsWrapper = styled.div`
 
   & > * {
     flex-grow: 1;
-    border: 1px solid #e3e6e8;
-    border-radius: 8px;
-    padding: 16px;
-    & > h4 {
-      font-weight: 500;
-      font-size: 16px;
-      line-height: 24px;
-      padding-bottom: 16px;
-    }
+  }
+`;
+
+export const EditorContainer = styled.div`
+  border: 1px solid #e3e6e8;
+  border-radius: 8px;
+  margin-bottom: 16px;
+  padding: 16px;
+  & > h4 {
+    font-weight: 500;
+    font-size: 16px;
+    line-height: 24px;
+    padding-bottom: 16px;
   }
 `;

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

@@ -1,175 +1,162 @@
 import React from 'react';
-import { useHistory } from 'react-router';
+import { useHistory, useParams } from 'react-router';
 import { useForm, Controller, FormProvider } from 'react-hook-form';
 import {
   CompatibilityLevelCompatibilityEnum,
-  SchemaSubject,
   SchemaType,
 } from 'generated-sources';
 import { clusterSchemaPath } from 'lib/paths';
-import { ClusterName, NewSchemaSubjectRaw, SchemaName } from 'redux/interfaces';
-import PageLoader from 'components/common/PageLoader/PageLoader';
+import { NewSchemaSubjectRaw } from 'redux/interfaces';
 import JSONEditor from 'components/common/JSONEditor/JSONEditor';
 import Select from 'components/common/Select/Select';
 import { Button } from 'components/common/Button/Button';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import PageHeading from 'components/common/PageHeading/PageHeading';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import {
+  schemasApiClient,
+  selectSchemaById,
+} from 'redux/reducers/schemas/schemasSlice';
+import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
+import { getResponse } from 'lib/errorHandling';
 
-import { EditorsWrapper, EditWrapper } from './Edit.styled';
-
-export interface EditProps {
-  subject: SchemaName;
-  schema: SchemaSubject;
-  clusterName: ClusterName;
-  schemasAreFetched: boolean;
-  fetchSchemasByClusterName: (clusterName: ClusterName) => void;
-  updateSchema: (
-    latestSchema: SchemaSubject,
-    newSchema: string,
-    newSchemaType: SchemaType,
-    newCompatibilityLevel: CompatibilityLevelCompatibilityEnum,
-    clusterName: string,
-    subject: string
-  ) => Promise<void>;
-}
+import * as S from './Edit.styled';
 
-const Edit = ({
-  subject,
-  schema,
-  clusterName,
-  schemasAreFetched,
-  fetchSchemasByClusterName,
-  updateSchema,
-}: EditProps) => {
-  React.useEffect(() => {
-    if (!schemasAreFetched) fetchSchemasByClusterName(clusterName);
-  }, [clusterName, fetchSchemasByClusterName]);
+const Edit: React.FC = () => {
+  const history = useHistory();
+  const dispatch = useAppDispatch();
 
+  const { clusterName, subject } =
+    useParams<{ clusterName: string; subject: string }>();
   const methods = useForm<NewSchemaSubjectRaw>({ mode: 'onChange' });
+  const {
+    formState: { isDirty, isSubmitting, dirtyFields },
+    control,
+    handleSubmit,
+  } = methods;
 
-  const getFormattedSchema = React.useCallback(
-    () => JSON.stringify(JSON.parse(schema.schema), null, '\t'),
+  const schema = useAppSelector((state) => selectSchemaById(state, subject));
+
+  const formatedSchema = React.useMemo(
+    () => JSON.stringify(JSON.parse(schema?.schema || '{}'), null, '\t'),
     [schema]
   );
-  const history = useHistory();
-  const onSubmit = React.useCallback(
-    async ({
-      schemaType,
-      compatibilityLevel,
-      newSchema,
-    }: {
-      schemaType: SchemaType;
-      compatibilityLevel: CompatibilityLevelCompatibilityEnum;
-      newSchema: string;
-    }) => {
-      try {
-        await updateSchema(
-          schema,
-          newSchema,
-          schemaType,
-          compatibilityLevel,
+
+  const onSubmit = React.useCallback(async (props: NewSchemaSubjectRaw) => {
+    if (!schema) return;
+
+    try {
+      if (dirtyFields.newSchema || dirtyFields.schemaType) {
+        await schemasApiClient.createNewSchema({
           clusterName,
-          subject
-        );
-        history.push(clusterSchemaPath(clusterName, subject));
-      } catch (e) {
-        // do not redirect
+          newSchemaSubject: {
+            ...schema,
+            schema: props.newSchema || schema.schema,
+            schemaType: props.schemaType || schema.schemaType,
+          },
+        });
+      }
+
+      if (dirtyFields.compatibilityLevel) {
+        await schemasApiClient.updateSchemaCompatibilityLevel({
+          clusterName,
+          subject,
+          compatibilityLevel: {
+            compatibility: props.compatibilityLevel,
+          },
+        });
       }
-    },
-    [
-      schema,
-      methods.register,
-      methods.control,
-      clusterName,
-      subject,
-      updateSchema,
-      history,
-    ]
-  );
+
+      history.push(clusterSchemaPath(clusterName, subject));
+    } catch (e) {
+      const err = await getResponse(e as Response);
+      dispatch(serverErrorAlertAdded(err));
+    }
+  }, []);
+
+  if (!schema) return null;
 
   return (
     <FormProvider {...methods}>
       <PageHeading text="Edit schema" />
-      {schemasAreFetched ? (
-        <EditWrapper>
-          <form onSubmit={methods.handleSubmit(onSubmit)}>
+      <S.EditWrapper>
+        <form onSubmit={handleSubmit(onSubmit)}>
+          <div>
             <div>
-              <div>
-                <InputLabel>Type</InputLabel>
-                <Select
-                  name="schemaType"
-                  required
-                  defaultValue={schema.schemaType}
-                  disabled={methods.formState.isSubmitting}
-                >
-                  {Object.keys(SchemaType).map((type: string) => (
-                    <option key={type} value={type}>
-                      {type}
-                    </option>
-                  ))}
-                </Select>
-              </div>
+              <InputLabel>Type</InputLabel>
+              <Select
+                name="schemaType"
+                required
+                defaultValue={schema.schemaType}
+                disabled={isSubmitting}
+              >
+                {Object.keys(SchemaType).map((type: string) => (
+                  <option key={type} value={type}>
+                    {type}
+                  </option>
+                ))}
+              </Select>
+            </div>
 
-              <div>
-                <InputLabel>Compatibility level</InputLabel>
-                <Select
-                  name="compatibilityLevel"
-                  defaultValue={schema.compatibilityLevel}
-                  disabled={methods.formState.isSubmitting}
-                >
-                  {Object.keys(CompatibilityLevelCompatibilityEnum).map(
-                    (level: string) => (
-                      <option key={level} value={level}>
-                        {level}
-                      </option>
-                    )
-                  )}
-                </Select>
-              </div>
+            <div>
+              <InputLabel>Compatibility level</InputLabel>
+              <Select
+                name="compatibilityLevel"
+                defaultValue={schema.compatibilityLevel}
+                disabled={isSubmitting}
+              >
+                {Object.keys(CompatibilityLevelCompatibilityEnum).map(
+                  (level: string) => (
+                    <option key={level} value={level}>
+                      {level}
+                    </option>
+                  )
+                )}
+              </Select>
             </div>
-            <EditorsWrapper>
-              <div>
+          </div>
+          <S.EditorsWrapper>
+            <div>
+              <S.EditorContainer>
                 <h4>Latest schema</h4>
                 <JSONEditor
                   isFixedHeight
                   readOnly
                   height="372px"
-                  value={getFormattedSchema()}
+                  value={formatedSchema}
                   name="latestSchema"
                   highlightActiveLine={false}
                 />
-              </div>
-              <div>
+              </S.EditorContainer>
+            </div>
+            <div>
+              <S.EditorContainer>
                 <h4>New schema</h4>
                 <Controller
-                  control={methods.control}
+                  control={control}
                   name="newSchema"
                   render={({ field: { name, onChange } }) => (
                     <JSONEditor
-                      readOnly={methods.formState.isSubmitting}
-                      defaultValue={getFormattedSchema()}
+                      readOnly={isSubmitting}
+                      defaultValue={formatedSchema}
                       name={name}
                       onChange={onChange}
                     />
                   )}
                 />
-              </div>
-            </EditorsWrapper>
-            <Button
-              buttonType="primary"
-              buttonSize="M"
-              type="submit"
-              disabled={
-                !methods.formState.isDirty || methods.formState.isSubmitting
-              }
-            >
-              Submit
-            </Button>
-          </form>
-        </EditWrapper>
-      ) : (
-        <PageLoader />
-      )}
+              </S.EditorContainer>
+              <Button
+                buttonType="primary"
+                buttonSize="M"
+                type="submit"
+                disabled={!isDirty || isSubmitting}
+              >
+                Submit
+              </Button>
+            </div>
+          </S.EditorsWrapper>
+        </form>
+      </S.EditWrapper>
     </FormProvider>
   );
 };

+ 0 - 38
kafka-ui-react-app/src/components/Schemas/Edit/EditContainer.ts

@@ -1,38 +0,0 @@
-import { connect } from 'react-redux';
-import { ClusterName, RootState } from 'redux/interfaces';
-import { RouteComponentProps, withRouter } from 'react-router-dom';
-import {
-  getIsSchemaListFetched,
-  getSchema,
-} from 'redux/reducers/schemas/selectors';
-import { fetchSchemasByClusterName, updateSchema } from 'redux/actions';
-
-import Edit from './Edit';
-
-interface RouteProps {
-  clusterName: ClusterName;
-  subject: string;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { clusterName, subject },
-    },
-  }: OwnProps
-) => ({
-  subject,
-  schema: getSchema(state, subject),
-  schemasAreFetched: getIsSchemaListFetched(state),
-  clusterName,
-});
-
-const mapDispatchToProps = {
-  fetchSchemasByClusterName,
-  updateSchema,
-};
-
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));

+ 39 - 66
kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx

@@ -1,78 +1,51 @@
-import { mount, shallow } from 'enzyme';
-import { SchemaType } from 'generated-sources';
 import React from 'react';
-import { StaticRouter } from 'react-router-dom';
-import Edit, { EditProps } from 'components/Schemas/Edit/Edit';
-import { ThemeProvider } from 'styled-components';
-import theme from 'theme/theme';
+import Edit from 'components/Schemas/Edit/Edit';
+import { render } from 'lib/testHelpers';
+import { clusterSchemaEditPath } from 'lib/paths';
+import {
+  schemasFulfilledState,
+  schemaVersion,
+} from 'redux/reducers/schemas/__test__/fixtures';
+import { Route } from 'react-router';
+import { screen } from '@testing-library/dom';
 
-jest.mock('react-hook-form', () => ({
-  ...jest.requireActual('react-hook-form'),
-  Controller: () => 'Controller',
-}));
+const clusterName = 'local';
+const { subject } = schemaVersion;
 
 describe('Edit Component', () => {
-  const mockSchema = {
-    subject: 'Subject',
-    version: '1',
-    id: 1,
-    schema: '{"schema": "schema"}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.AVRO,
-  };
-
-  const setupWrapper = (props: Partial<EditProps> = {}) => (
-    <Edit
-      subject="Subject"
-      clusterName="ClusterName"
-      schemasAreFetched
-      fetchSchemasByClusterName={jest.fn()}
-      updateSchema={jest.fn()}
-      schema={mockSchema}
-      {...props}
-    />
-  );
-
-  describe('when schemas are not fetched', () => {
-    const component = shallow(setupWrapper({ schemasAreFetched: false }));
-    it('matches the snapshot', () => {
-      expect(component).toMatchSnapshot();
-    });
-    it('shows loader', () => {
-      expect(component.find('PageLoader').exists()).toBeTruthy();
-    });
-    it('fetches them', () => {
-      const mockFetch = jest.fn();
-      mount(
-        <ThemeProvider theme={theme}>
-          <StaticRouter>
-            {setupWrapper({
-              schemasAreFetched: false,
-              fetchSchemasByClusterName: mockFetch,
-            })}
-          </StaticRouter>
-        </ThemeProvider>
+  describe('schema exists', () => {
+    beforeEach(() => {
+      render(
+        <Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
+          <Edit />
+        </Route>,
+        {
+          pathname: clusterSchemaEditPath(clusterName, subject),
+          preloadedState: { schemas: schemasFulfilledState },
+        }
       );
-      expect(mockFetch).toHaveBeenCalledTimes(1);
     });
-  });
 
-  describe('when schemas are fetched', () => {
-    const component = shallow(setupWrapper());
-    it('matches the snapshot', () => {
-      expect(component).toMatchSnapshot();
+    it('renders component', () => {
+      expect(screen.getByText('Edit schema')).toBeInTheDocument();
     });
-    it('shows editor', () => {
-      expect(
-        component.find('Styled(JSONEditor)[name="latestSchema"]').length
-      ).toEqual(1);
-      expect(component.find('Controller').length).toEqual(1);
-      expect(component.find('Button').exists()).toBeTruthy();
+  });
+
+  describe('schema does not exist', () => {
+    beforeEach(() => {
+      render(
+        <Route path={clusterSchemaEditPath(':clusterName', ':subject')}>
+          <Edit />
+        </Route>,
+        {
+          pathname: clusterSchemaEditPath(clusterName, 'fake'),
+          preloadedState: { schemas: schemasFulfilledState },
+        }
+      );
     });
-    it('does not fetch them', () => {
-      const mockFetch = jest.fn();
-      shallow(setupWrapper());
-      expect(mockFetch).toHaveBeenCalledTimes(0);
+
+    it('renders component', () => {
+      expect(screen.queryByText('Edit schema')).not.toBeInTheDocument();
     });
   });
 });

+ 0 - 441
kafka-ui-react-app/src/components/Schemas/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap

@@ -1,441 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Edit Component when schemas are fetched matches the snapshot 1`] = `
-<Component
-  clearErrors={[Function]}
-  control={
-    Object {
-      "controllerSubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-      "defaultValuesRef": Object {
-        "current": Object {},
-      },
-      "fieldArrayDefaultValuesRef": Object {
-        "current": Object {},
-      },
-      "fieldArrayNamesRef": Object {
-        "current": Set {},
-      },
-      "fieldArraySubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-      "fieldsRef": Object {
-        "current": Object {},
-      },
-      "fieldsWithValidationRef": Object {
-        "current": Object {},
-      },
-      "formStateRef": Object {
-        "current": Object {
-          "dirtyFields": Object {},
-          "errors": Object {},
-          "isDirty": false,
-          "isSubmitSuccessful": false,
-          "isSubmitted": false,
-          "isSubmitting": false,
-          "isValid": true,
-          "isValidating": false,
-          "submitCount": 0,
-          "touchedFields": Object {},
-        },
-      },
-      "formStateSubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-      "getIsDirty": [Function],
-      "inFieldArrayActionRef": Object {
-        "current": false,
-      },
-      "isWatchAllRef": Object {
-        "current": false,
-      },
-      "readFormStateRef": Object {
-        "current": Object {
-          "dirtyFields": false,
-          "errors": false,
-          "isDirty": "all",
-          "isSubmitting": "all",
-          "isValid": false,
-          "isValidating": false,
-          "touchedFields": false,
-        },
-      },
-      "register": [Function],
-      "shouldUnmount": undefined,
-      "unregister": [Function],
-      "validFieldsRef": Object {
-        "current": Object {},
-      },
-      "watchFieldsRef": Object {
-        "current": Set {},
-      },
-      "watchInternal": [Function],
-      "watchSubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-    }
-  }
-  formState={
-    Object {
-      "dirtyFields": Object {},
-      "errors": Object {},
-      "isDirty": false,
-      "isSubmitSuccessful": false,
-      "isSubmitted": false,
-      "isSubmitting": false,
-      "isValid": true,
-      "isValidating": false,
-      "submitCount": 0,
-      "touchedFields": Object {},
-    }
-  }
-  getValues={[Function]}
-  handleSubmit={[Function]}
-  register={[Function]}
-  reset={[Function]}
-  setError={[Function]}
-  setFocus={[Function]}
-  setValue={[Function]}
-  trigger={[Function]}
-  unregister={[Function]}
-  watch={[Function]}
->
-  <Styled(PageHeading)
-    text="Edit schema"
-  />
-  <styled.div>
-    <form
-      onSubmit={[Function]}
-    >
-      <div>
-        <div>
-          <styled.label>
-            Type
-          </styled.label>
-          <Styled(Select)
-            defaultValue="AVRO"
-            disabled={false}
-            name="schemaType"
-            required={true}
-          >
-            <option
-              key="AVRO"
-              value="AVRO"
-            >
-              AVRO
-            </option>
-            <option
-              key="JSON"
-              value="JSON"
-            >
-              JSON
-            </option>
-            <option
-              key="PROTOBUF"
-              value="PROTOBUF"
-            >
-              PROTOBUF
-            </option>
-          </Styled(Select)>
-        </div>
-        <div>
-          <styled.label>
-            Compatibility level
-          </styled.label>
-          <Styled(Select)
-            defaultValue="BACKWARD"
-            disabled={false}
-            name="compatibilityLevel"
-          >
-            <option
-              key="BACKWARD"
-              value="BACKWARD"
-            >
-              BACKWARD
-            </option>
-            <option
-              key="BACKWARD_TRANSITIVE"
-              value="BACKWARD_TRANSITIVE"
-            >
-              BACKWARD_TRANSITIVE
-            </option>
-            <option
-              key="FORWARD"
-              value="FORWARD"
-            >
-              FORWARD
-            </option>
-            <option
-              key="FORWARD_TRANSITIVE"
-              value="FORWARD_TRANSITIVE"
-            >
-              FORWARD_TRANSITIVE
-            </option>
-            <option
-              key="FULL"
-              value="FULL"
-            >
-              FULL
-            </option>
-            <option
-              key="FULL_TRANSITIVE"
-              value="FULL_TRANSITIVE"
-            >
-              FULL_TRANSITIVE
-            </option>
-            <option
-              key="NONE"
-              value="NONE"
-            >
-              NONE
-            </option>
-          </Styled(Select)>
-        </div>
-      </div>
-      <styled.div>
-        <div>
-          <h4>
-            Latest schema
-          </h4>
-          <Styled(JSONEditor)
-            height="372px"
-            highlightActiveLine={false}
-            isFixedHeight={true}
-            name="latestSchema"
-            readOnly={true}
-            value="{
-	\\"schema\\": \\"schema\\"
-}"
-          />
-        </div>
-        <div>
-          <h4>
-            New schema
-          </h4>
-          <Controller
-            control={
-              Object {
-                "controllerSubjectRef": Object {
-                  "current": Re {
-                    "observers": Array [],
-                  },
-                },
-                "defaultValuesRef": Object {
-                  "current": Object {},
-                },
-                "fieldArrayDefaultValuesRef": Object {
-                  "current": Object {},
-                },
-                "fieldArrayNamesRef": Object {
-                  "current": Set {},
-                },
-                "fieldArraySubjectRef": Object {
-                  "current": Re {
-                    "observers": Array [],
-                  },
-                },
-                "fieldsRef": Object {
-                  "current": Object {},
-                },
-                "fieldsWithValidationRef": Object {
-                  "current": Object {},
-                },
-                "formStateRef": Object {
-                  "current": Object {
-                    "dirtyFields": Object {},
-                    "errors": Object {},
-                    "isDirty": false,
-                    "isSubmitSuccessful": false,
-                    "isSubmitted": false,
-                    "isSubmitting": false,
-                    "isValid": true,
-                    "isValidating": false,
-                    "submitCount": 0,
-                    "touchedFields": Object {},
-                  },
-                },
-                "formStateSubjectRef": Object {
-                  "current": Re {
-                    "observers": Array [],
-                  },
-                },
-                "getIsDirty": [Function],
-                "inFieldArrayActionRef": Object {
-                  "current": false,
-                },
-                "isWatchAllRef": Object {
-                  "current": false,
-                },
-                "readFormStateRef": Object {
-                  "current": Object {
-                    "constructor": "all",
-                    "dirtyFields": "all",
-                    "errors": "all",
-                    "isDirty": "all",
-                    "isSubmitSuccessful": "all",
-                    "isSubmitted": "all",
-                    "isSubmitting": "all",
-                    "isValid": "all",
-                    "isValidating": "all",
-                    "submitCount": "all",
-                    "touchedFields": "all",
-                  },
-                },
-                "register": [Function],
-                "shouldUnmount": undefined,
-                "unregister": [Function],
-                "validFieldsRef": Object {
-                  "current": Object {},
-                },
-                "watchFieldsRef": Object {
-                  "current": Set {},
-                },
-                "watchInternal": [Function],
-                "watchSubjectRef": Object {
-                  "current": Re {
-                    "observers": Array [],
-                  },
-                },
-              }
-            }
-            name="newSchema"
-            render={[Function]}
-          />
-        </div>
-      </styled.div>
-      <Button
-        buttonSize="M"
-        buttonType="primary"
-        disabled={true}
-        type="submit"
-      >
-        Submit
-      </Button>
-    </form>
-  </styled.div>
-</Component>
-`;
-
-exports[`Edit Component when schemas are not fetched matches the snapshot 1`] = `
-<Component
-  clearErrors={[Function]}
-  control={
-    Object {
-      "controllerSubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-      "defaultValuesRef": Object {
-        "current": Object {},
-      },
-      "fieldArrayDefaultValuesRef": Object {
-        "current": Object {},
-      },
-      "fieldArrayNamesRef": Object {
-        "current": Set {},
-      },
-      "fieldArraySubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-      "fieldsRef": Object {
-        "current": Object {},
-      },
-      "fieldsWithValidationRef": Object {
-        "current": Object {},
-      },
-      "formStateRef": Object {
-        "current": Object {
-          "dirtyFields": Object {},
-          "errors": Object {},
-          "isDirty": false,
-          "isSubmitSuccessful": false,
-          "isSubmitted": false,
-          "isSubmitting": false,
-          "isValid": true,
-          "isValidating": false,
-          "submitCount": 0,
-          "touchedFields": Object {},
-        },
-      },
-      "formStateSubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-      "getIsDirty": [Function],
-      "inFieldArrayActionRef": Object {
-        "current": false,
-      },
-      "isWatchAllRef": Object {
-        "current": false,
-      },
-      "readFormStateRef": Object {
-        "current": Object {
-          "dirtyFields": false,
-          "errors": false,
-          "isDirty": false,
-          "isValid": false,
-          "isValidating": false,
-          "touchedFields": false,
-        },
-      },
-      "register": [Function],
-      "shouldUnmount": undefined,
-      "unregister": [Function],
-      "validFieldsRef": Object {
-        "current": Object {},
-      },
-      "watchFieldsRef": Object {
-        "current": Set {},
-      },
-      "watchInternal": [Function],
-      "watchSubjectRef": Object {
-        "current": Re {
-          "observers": Array [],
-        },
-      },
-    }
-  }
-  formState={
-    Object {
-      "dirtyFields": Object {},
-      "errors": Object {},
-      "isDirty": false,
-      "isSubmitSuccessful": false,
-      "isSubmitted": false,
-      "isSubmitting": false,
-      "isValid": true,
-      "isValidating": false,
-      "submitCount": 0,
-      "touchedFields": Object {},
-    }
-  }
-  getValues={[Function]}
-  handleSubmit={[Function]}
-  register={[Function]}
-  reset={[Function]}
-  setError={[Function]}
-  setFocus={[Function]}
-  setValue={[Function]}
-  trigger={[Function]}
-  unregister={[Function]}
-  watch={[Function]}
->
-  <Styled(PageHeading)
-    text="Edit schema"
-  />
-  <PageLoader />
-</Component>
-`;

+ 1 - 1
kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.styled.ts

@@ -1,6 +1,6 @@
 import styled from 'styled-components';
 
-export const GlobalSchemaSelectorWrapper = styled.div`
+export const Wrapper = styled.div`
   display: flex;
   gap: 5px;
   align-items: center;

+ 89 - 56
kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector.tsx

@@ -1,73 +1,106 @@
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
 import Select from 'components/common/Select/Select';
 import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
+import { getResponse } from 'lib/errorHandling';
+import { useAppDispatch } from 'lib/hooks/redux';
 import React from 'react';
-import { FormProvider, useForm } from 'react-hook-form';
 import { useParams } from 'react-router-dom';
-import { ClusterName } from 'redux/interfaces';
+import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
+import {
+  fetchSchemas,
+  schemasApiClient,
+} from 'redux/reducers/schemas/schemasSlice';
 
-import { GlobalSchemaSelectorWrapper } from './GlobalSchemaSelector.styled';
+import * as S from './GlobalSchemaSelector.styled';
 
-export interface GlobalSchemaSelectorProps {
-  globalSchemaCompatibilityLevel?: CompatibilityLevelCompatibilityEnum;
-  updateGlobalSchemaCompatibilityLevel: (
-    clusterName: ClusterName,
-    compatibilityLevel: CompatibilityLevelCompatibilityEnum
-  ) => Promise<void>;
-}
-
-const GlobalSchemaSelector: React.FC<GlobalSchemaSelectorProps> = ({
-  globalSchemaCompatibilityLevel,
-  updateGlobalSchemaCompatibilityLevel,
-}) => {
+const GlobalSchemaSelector: React.FC = () => {
   const { clusterName } = useParams<{ clusterName: string }>();
+  const dispatch = useAppDispatch();
+  const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
+    React.useState<CompatibilityLevelCompatibilityEnum | undefined>();
+  const [nextCompatibilityLevel, setNextCompatibilityLevel] = React.useState<
+    CompatibilityLevelCompatibilityEnum | undefined
+  >();
+
+  const [isFetching, setIsFetching] = React.useState(false);
+  const [isUpdating, setIsUpdating] = React.useState(false);
+  const [isConfirmationVisible, setIsConfirmationVisible] =
+    React.useState(false);
 
-  const methods = useForm();
+  React.useEffect(() => {
+    const fetchData = async () => {
+      setIsFetching(true);
+      try {
+        const { compatibility } =
+          await schemasApiClient.getGlobalSchemaCompatibilityLevel({
+            clusterName,
+          });
+        setCurrentCompatibilityLevel(compatibility);
+      } catch (error) {
+        // do nothing
+      }
+      setIsFetching(false);
+    };
 
-  const [
-    isUpdateCompatibilityConfirmationVisible,
-    setUpdateCompatibilityConfirmationVisible,
-  ] = React.useState(false);
+    fetchData();
+  }, []);
 
-  const onCompatibilityLevelUpdate = async ({
-    compatibilityLevel,
-  }: {
-    compatibilityLevel: CompatibilityLevelCompatibilityEnum;
-  }) => {
-    await updateGlobalSchemaCompatibilityLevel(clusterName, compatibilityLevel);
-    setUpdateCompatibilityConfirmationVisible(false);
+  const handleChangeCompatibilityLevel = (
+    event: React.ChangeEvent<HTMLSelectElement>
+  ) => {
+    setNextCompatibilityLevel(
+      event.target.value as CompatibilityLevelCompatibilityEnum
+    );
+    setIsConfirmationVisible(true);
   };
 
+  const handleUpdateCompatibilityLevel = async () => {
+    setIsUpdating(true);
+    if (nextCompatibilityLevel) {
+      try {
+        await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
+          clusterName,
+          compatibilityLevel: { compatibility: nextCompatibilityLevel },
+        });
+        dispatch(fetchSchemas(clusterName));
+      } catch (e) {
+        const err = await getResponse(e as Response);
+        dispatch(serverErrorAlertAdded(err));
+      }
+    }
+    setIsUpdating(false);
+  };
+
+  if (!currentCompatibilityLevel) return null;
+
   return (
-    <FormProvider {...methods}>
-      <GlobalSchemaSelectorWrapper>
-        <h5>Global Compatibility Level: </h5>
-        <Select
-          name="compatibilityLevel"
-          selectSize="M"
-          defaultValue={globalSchemaCompatibilityLevel}
-          onChange={() => setUpdateCompatibilityConfirmationVisible(true)}
-          disabled={methods.formState.isSubmitting}
-        >
-          {Object.keys(CompatibilityLevelCompatibilityEnum).map(
-            (level: string) => (
-              <option key={level} value={level}>
-                {level}
-              </option>
-            )
-          )}
-        </Select>
-        <ConfirmationModal
-          isOpen={isUpdateCompatibilityConfirmationVisible}
-          onCancel={() => setUpdateCompatibilityConfirmationVisible(false)}
-          onConfirm={methods.handleSubmit(onCompatibilityLevelUpdate)}
-          isConfirming={methods.formState.isSubmitting}
-        >
-          Are you sure you want to update the global compatibility level? This
-          may affect the compatibility levels of the schemas.
-        </ConfirmationModal>
-      </GlobalSchemaSelectorWrapper>
-    </FormProvider>
+    <S.Wrapper>
+      <div>Global Compatibility Level: </div>
+      <Select
+        selectSize="M"
+        defaultValue={currentCompatibilityLevel}
+        onChange={handleChangeCompatibilityLevel}
+        disabled={isFetching || isUpdating || isConfirmationVisible}
+      >
+        {Object.keys(CompatibilityLevelCompatibilityEnum).map(
+          (level: string) => (
+            <option key={level} value={level}>
+              {level}
+            </option>
+          )
+        )}
+      </Select>
+      <ConfirmationModal
+        isOpen={isConfirmationVisible}
+        onCancel={() => setIsConfirmationVisible(false)}
+        onConfirm={handleUpdateCompatibilityLevel}
+        isConfirming={isUpdating}
+      >
+        Are you sure you want to update the global compatibility level and set
+        it to <b>{nextCompatibilityLevel}</b>? This may affect the compatibility
+        levels of the schemas.
+      </ConfirmationModal>
+    </S.Wrapper>
   );
 };
 

+ 31 - 74
kafka-ui-react-app/src/components/Schemas/List/List.tsx

@@ -1,64 +1,28 @@
 import React from 'react';
-import {
-  CompatibilityLevelCompatibilityEnum,
-  SchemaSubject,
-} from 'generated-sources';
 import { useParams } from 'react-router-dom';
 import { clusterSchemaNewPath } from 'lib/paths';
-import { ClusterName } from 'redux/interfaces';
-import PageLoader from 'components/common/PageLoader/PageLoader';
 import ClusterContext from 'components/contexts/ClusterContext';
-import { Table } from 'components/common/table/Table/Table.styled';
+import * as C from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import { Button } from 'components/common/Button/Button';
 import PageHeading from 'components/common/PageHeading/PageHeading';
+import { useAppSelector } from 'lib/hooks/redux';
+import { selectAllSchemas } from 'redux/reducers/schemas/schemasSlice';
 
 import ListItem from './ListItem';
 import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
 
-export interface ListProps {
-  schemas: SchemaSubject[];
-  isFetching: boolean;
-  isGlobalSchemaCompatibilityLevelFetched: boolean;
-  globalSchemaCompatibilityLevel?: CompatibilityLevelCompatibilityEnum;
-  fetchSchemasByClusterName: (clusterName: ClusterName) => void;
-  fetchGlobalSchemaCompatibilityLevel: (
-    clusterName: ClusterName
-  ) => Promise<void>;
-  updateGlobalSchemaCompatibilityLevel: (
-    clusterName: ClusterName,
-    compatibilityLevel: CompatibilityLevelCompatibilityEnum
-  ) => Promise<void>;
-}
-
-const List: React.FC<ListProps> = ({
-  schemas,
-  isFetching,
-  globalSchemaCompatibilityLevel,
-  isGlobalSchemaCompatibilityLevelFetched,
-  fetchSchemasByClusterName,
-  fetchGlobalSchemaCompatibilityLevel,
-  updateGlobalSchemaCompatibilityLevel,
-}) => {
+const List: React.FC = () => {
   const { isReadOnly } = React.useContext(ClusterContext);
   const { clusterName } = useParams<{ clusterName: string }>();
-
-  React.useEffect(() => {
-    fetchSchemasByClusterName(clusterName);
-    fetchGlobalSchemaCompatibilityLevel(clusterName);
-  }, [fetchSchemasByClusterName, clusterName]);
+  const schemas = useAppSelector(selectAllSchemas);
 
   return (
-    <div>
+    <>
       <PageHeading text="Schema Registry">
-        {!isReadOnly && isGlobalSchemaCompatibilityLevelFetched && (
+        {!isReadOnly && (
           <>
-            <GlobalSchemaSelector
-              globalSchemaCompatibilityLevel={globalSchemaCompatibilityLevel}
-              updateGlobalSchemaCompatibilityLevel={
-                updateGlobalSchemaCompatibilityLevel
-              }
-            />
+            <GlobalSchemaSelector />
             <Button
               buttonSize="M"
               buttonType="primary"
@@ -70,36 +34,29 @@ const List: React.FC<ListProps> = ({
           </>
         )}
       </PageHeading>
-
-      {isFetching ? (
-        <PageLoader />
-      ) : (
-        <div>
-          <Table isFullwidth>
-            <thead>
-              <tr>
-                <TableHeaderCell title="Schema Name" />
-                <TableHeaderCell title="Version" />
-                <TableHeaderCell title="Compatibility" />
-              </tr>
-            </thead>
-            <tbody>
-              {schemas.length === 0 && (
-                <tr>
-                  <td colSpan={10}>No schemas found</td>
-                </tr>
-              )}
-              {schemas.map((subject) => (
-                <ListItem
-                  key={[subject.id, subject.subject].join('-')}
-                  subject={subject}
-                />
-              ))}
-            </tbody>
-          </Table>
-        </div>
-      )}
-    </div>
+      <C.Table isFullwidth>
+        <thead>
+          <tr>
+            <TableHeaderCell title="Schema Name" />
+            <TableHeaderCell title="Version" />
+            <TableHeaderCell title="Compatibility" />
+          </tr>
+        </thead>
+        <tbody>
+          {schemas.length === 0 && (
+            <tr>
+              <td colSpan={10}>No schemas found</td>
+            </tr>
+          )}
+          {schemas.map((subject) => (
+            <ListItem
+              key={[subject.id, subject.subject].join('-')}
+              subject={subject}
+            />
+          ))}
+        </tbody>
+      </C.Table>
+    </>
   );
 };
 

+ 0 - 31
kafka-ui-react-app/src/components/Schemas/List/ListContainer.tsx

@@ -1,31 +0,0 @@
-import { connect } from 'react-redux';
-import { RootState } from 'redux/interfaces';
-import {
-  fetchSchemasByClusterName,
-  fetchGlobalSchemaCompatibilityLevel,
-  updateGlobalSchemaCompatibilityLevel,
-} from 'redux/actions';
-import {
-  getIsSchemaListFetching,
-  getSchemaList,
-  getGlobalSchemaCompatibilityLevel,
-  getGlobalSchemaCompatibilityLevelFetched,
-} from 'redux/reducers/schemas/selectors';
-
-import List from './List';
-
-const mapStateToProps = (state: RootState) => ({
-  isFetching: getIsSchemaListFetching(state),
-  schemas: getSchemaList(state),
-  globalSchemaCompatibilityLevel: getGlobalSchemaCompatibilityLevel(state),
-  isGlobalSchemaCompatibilityLevelFetched:
-    getGlobalSchemaCompatibilityLevelFetched(state),
-});
-
-const mapDispatchToProps = {
-  fetchGlobalSchemaCompatibilityLevel,
-  fetchSchemasByClusterName,
-  updateGlobalSchemaCompatibilityLevel,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(List);

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

@@ -1,7 +1,7 @@
 import React from 'react';
 import { SchemaSubject } from 'generated-sources';
 import { NavLink } from 'react-router-dom';
-import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
+import * as S from 'components/common/table/Table/TableKeyLink.styled';
 
 export interface ListItemProps {
   subject: SchemaSubject;
@@ -12,13 +12,13 @@ const ListItem: React.FC<ListItemProps> = ({
 }) => {
   return (
     <tr>
-      <TableKeyLink>
-        <NavLink exact to={`schemas/${subject}`}>
+      <S.TableKeyLink>
+        <NavLink exact to={`schemas/${subject}`} role="link">
           {subject}
         </NavLink>
-      </TableKeyLink>
-      <td>{version}</td>
-      <td>{compatibilityLevel}</td>
+      </S.TableKeyLink>
+      <td role="cell">{version}</td>
+      <td role="cell">{compatibilityLevel}</td>
     </tr>
   );
 };

+ 51 - 103
kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx

@@ -1,114 +1,62 @@
 import React from 'react';
-import { mount, shallow } from 'enzyme';
-import { Provider } from 'react-redux';
-import { StaticRouter } from 'react-router';
-import { store } from 'redux/store';
-import ClusterContext from 'components/contexts/ClusterContext';
-import ListContainer from 'components/Schemas/List/ListContainer';
-import List, { ListProps } from 'components/Schemas/List/List';
-import { ThemeProvider } from 'styled-components';
-import theme from 'theme/theme';
-
-import { schemas } from './fixtures';
+import List from 'components/Schemas/List/List';
+import { render } from 'lib/testHelpers';
+import { Route } from 'react-router';
+import { clusterSchemasPath } from 'lib/paths';
+import { screen } from '@testing-library/dom';
+import {
+  schemasFulfilledState,
+  schemasInitialState,
+} from 'redux/reducers/schemas/__test__/fixtures';
+import ClusterContext, {
+  ContextProps,
+  initialValue as contextInitialValue,
+} from 'components/contexts/ClusterContext';
+import { RootState } from 'redux/interfaces';
+
+const clusterName = 'testClusterName';
+
+const renderComponent = (
+  initialState: RootState['schemas'] = schemasInitialState,
+  context: ContextProps = contextInitialValue
+) =>
+  render(
+    <ClusterContext.Provider value={context}>
+      <Route path={clusterSchemasPath(':clusterName')}>
+        <List />
+      </Route>
+    </ClusterContext.Provider>,
+    {
+      pathname: clusterSchemasPath(clusterName),
+      preloadedState: {
+        loader: {
+          'schemas/fetch': 'fulfilled',
+        },
+        schemas: initialState,
+      },
+    }
+  );
 
 describe('List', () => {
-  describe('Container', () => {
-    it('renders view', () => {
-      const component = shallow(
-        <Provider store={store}>
-          <ListContainer />
-        </Provider>
-      );
-
-      expect(component.exists()).toBeTruthy();
-    });
+  it('renders list', () => {
+    renderComponent(schemasFulfilledState);
+    expect(screen.getByText('MySchemaSubject')).toBeInTheDocument();
+    expect(screen.getByText('schema7_1')).toBeInTheDocument();
   });
 
-  describe('View', () => {
-    const pathname = `/ui/clusters/clusterName/schemas`;
-
-    const setupWrapper = (props: Partial<ListProps> = {}) => (
-      <ThemeProvider theme={theme}>
-        <StaticRouter location={{ pathname }} context={{}}>
-          <List
-            isFetching
-            fetchSchemasByClusterName={jest.fn()}
-            isGlobalSchemaCompatibilityLevelFetched
-            fetchGlobalSchemaCompatibilityLevel={jest.fn()}
-            updateGlobalSchemaCompatibilityLevel={jest.fn()}
-            schemas={[]}
-            {...props}
-          />
-        </StaticRouter>
-      </ThemeProvider>
-    );
-
-    describe('Initial state', () => {
-      let useEffect: jest.SpyInstance<
-        void,
-        [effect: React.EffectCallback, deps?: React.DependencyList | undefined]
-      >;
-      const mockedFn = jest.fn();
-
-      const mockedUseEffect = () => {
-        useEffect.mockImplementationOnce(mockedFn);
-      };
-
-      beforeEach(() => {
-        useEffect = jest.spyOn(React, 'useEffect');
-        mockedUseEffect();
-      });
-
-      it('should call fetchSchemasByClusterName every render', () => {
-        mount(setupWrapper({ fetchSchemasByClusterName: mockedFn }));
-        expect(mockedFn).toHaveBeenCalled();
-      });
-    });
-
-    describe('when fetching', () => {
-      it('renders PageLoader', () => {
-        const wrapper = mount(setupWrapper({ isFetching: true }));
-        expect(wrapper.exists('thead')).toBeFalsy();
-        expect(wrapper.exists('ListItem')).toBeFalsy();
-        expect(wrapper.exists('PageLoader')).toBeTruthy();
-      });
-    });
-
-    describe('without schemas', () => {
-      it('renders table heading without ListItem', () => {
-        const wrapper = mount(setupWrapper({ isFetching: false }));
-        expect(wrapper.exists('thead')).toBeTruthy();
-        expect(wrapper.exists('ListItem')).toBeFalsy();
-      });
-    });
-
-    describe('with schemas', () => {
-      const wrapper = mount(setupWrapper({ isFetching: false, schemas }));
+  it('renders empty table', () => {
+    renderComponent();
+    expect(screen.getByText('No schemas found')).toBeInTheDocument();
+  });
 
-      it('renders table heading with ListItem', () => {
-        expect(wrapper.exists('thead')).toBeTruthy();
-        expect(wrapper.find('ListItem').length).toEqual(3);
+  describe('with readonly cluster', () => {
+    it('does not render Create Schema button', () => {
+      renderComponent(schemasFulfilledState, {
+        ...contextInitialValue,
+        isReadOnly: true,
       });
-    });
 
-    describe('with readonly cluster', () => {
-      const wrapper = mount(
-        <StaticRouter>
-          <ClusterContext.Provider
-            value={{
-              isReadOnly: true,
-              hasKafkaConnectConfigured: true,
-              hasSchemaRegistryConfigured: true,
-              isTopicDeletionAllowed: true,
-            }}
-          >
-            {setupWrapper({ schemas: [] })}
-          </ClusterContext.Provider>
-        </StaticRouter>
-      );
-      it('does not render Create Schema button', () => {
-        expect(wrapper.exists('NavLink')).toBeFalsy();
-      });
+      expect(screen.queryByText('Create Schema')).not.toBeInTheDocument();
     });
   });
 });

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

@@ -1,27 +1,23 @@
 import React from 'react';
-import { mount } from 'enzyme';
-import { BrowserRouter as Router } from 'react-router-dom';
-import ListItem from 'components/Schemas/List/ListItem';
+import ListItem, { ListItemProps } from 'components/Schemas/List/ListItem';
+import { screen } from '@testing-library/react';
+import { render } from 'lib/testHelpers';
 
 import { schemas } from './fixtures';
 
 describe('ListItem', () => {
-  const wrapper = mount(
-    <Router>
+  const setupComponent = (props: ListItemProps = { subject: schemas[0] }) =>
+    render(
       <table>
         <tbody>
-          <ListItem subject={schemas[0]} />
+          <ListItem {...props} />
         </tbody>
       </table>
-    </Router>
-  );
+    );
 
   it('renders schemas', () => {
-    expect(wrapper.find('NavLink').length).toEqual(1);
-    expect(wrapper.find('td').length).toEqual(3);
-  });
-
-  it('matches snapshot', () => {
-    expect(wrapper).toMatchSnapshot();
+    setupComponent();
+    expect(screen.getAllByRole('link').length).toEqual(1);
+    expect(screen.getAllByRole('cell').length).toEqual(3);
   });
 });

+ 0 - 90
kafka-ui-react-app/src/components/Schemas/List/__test__/__snapshots__/ListItem.spec.tsx.snap

@@ -1,90 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ListItem matches snapshot 1`] = `
-<BrowserRouter>
-  <Router
-    history={
-      Object {
-        "action": "POP",
-        "block": [Function],
-        "createHref": [Function],
-        "go": [Function],
-        "goBack": [Function],
-        "goForward": [Function],
-        "length": 1,
-        "listen": [Function],
-        "location": Object {
-          "hash": "",
-          "pathname": "/",
-          "search": "",
-          "state": undefined,
-        },
-        "push": [Function],
-        "replace": [Function],
-      }
-    }
-  >
-    <table>
-      <tbody>
-        <ListItem
-          subject={
-            Object {
-              "compatibilityLevel": "BACKWARD",
-              "id": 1,
-              "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-              "schemaType": "JSON",
-              "subject": "test",
-              "version": "1",
-            }
-          }
-        >
-          <tr>
-            <styled.td>
-              <td
-                className="cQKBWR"
-              >
-                <NavLink
-                  exact={true}
-                  to="schemas/test"
-                >
-                  <Link
-                    aria-current={null}
-                    to={
-                      Object {
-                        "hash": "",
-                        "pathname": "/schemas/test",
-                        "search": "",
-                        "state": null,
-                      }
-                    }
-                  >
-                    <LinkAnchor
-                      aria-current={null}
-                      href="/schemas/test"
-                      navigate={[Function]}
-                    >
-                      <a
-                        aria-current={null}
-                        href="/schemas/test"
-                        onClick={[Function]}
-                      >
-                        test
-                      </a>
-                    </LinkAnchor>
-                  </Link>
-                </NavLink>
-              </td>
-            </styled.td>
-            <td>
-              1
-            </td>
-            <td>
-              BACKWARD
-            </td>
-          </tr>
-        </ListItem>
-      </tbody>
-    </table>
-  </Router>
-</BrowserRouter>
-`;

+ 20 - 0
kafka-ui-react-app/src/components/Schemas/New/New.styled.ts

@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+export const Form = styled.form`
+  padding: 16px;
+  padding-top: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+
+  & > button {
+    align-self: flex-start;
+  }
+
+  & textarea {
+    height: 200px;
+  }
+  & select {
+    width: 30%;
+  }
+`;

+ 20 - 36
kafka-ui-react-app/src/components/Schemas/New/New.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
-import { ClusterName, NewSchemaSubjectRaw } from 'redux/interfaces';
+import { NewSchemaSubjectRaw } from 'redux/interfaces';
 import { FormProvider, useForm } from 'react-hook-form';
 import { ErrorMessage } from '@hookform/error-message';
 import { clusterSchemaPath } from 'lib/paths';
-import { NewSchemaSubject, SchemaType } from 'generated-sources';
+import { SchemaType } from 'generated-sources';
 import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants';
 import { useHistory, useParams } from 'react-router';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
@@ -11,39 +11,22 @@ import Input from 'components/common/Input/Input';
 import { FormError } from 'components/common/Input/Input.styled';
 import Select from 'components/common/Select/Select';
 import { Button } from 'components/common/Button/Button';
-import styled from 'styled-components';
 import { Textarea } from 'components/common/Textbox/Textarea.styled';
 import PageHeading from 'components/common/PageHeading/PageHeading';
+import {
+  schemaAdded,
+  schemasApiClient,
+} from 'redux/reducers/schemas/schemasSlice';
+import { useAppDispatch } from 'lib/hooks/redux';
+import { serverErrorAlertAdded } from 'redux/reducers/alerts/alertsSlice';
+import { getResponse } from 'lib/errorHandling';
 
-export interface NewProps {
-  createSchema: (
-    clusterName: ClusterName,
-    newSchemaSubject: NewSchemaSubject
-  ) => Promise<void>;
-}
+import * as S from './New.styled';
 
-const NewSchemaFormStyled = styled.form`
-  padding: 16px;
-  padding-top: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-
-  & > button:last-child {
-    align-self: flex-start;
-  }
-
-  & textarea {
-    height: 200px;
-  }
-  & select {
-    width: 30%;
-  }
-`;
-
-const New: React.FC<NewProps> = ({ createSchema }) => {
+const New: React.FC = () => {
   const { clusterName } = useParams<{ clusterName: string }>();
   const history = useHistory();
+  const dispatch = useAppDispatch();
   const methods = useForm<NewSchemaSubjectRaw>();
   const {
     register,
@@ -54,14 +37,15 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
   const onSubmit = React.useCallback(
     async ({ subject, schema, schemaType }: NewSchemaSubjectRaw) => {
       try {
-        await createSchema(clusterName, {
-          subject,
-          schema,
-          schemaType,
+        const resp = await schemasApiClient.createNewSchema({
+          clusterName,
+          newSchemaSubject: { subject, schema, schemaType },
         });
+        dispatch(schemaAdded(resp));
         history.push(clusterSchemaPath(clusterName, subject));
       } catch (e) {
-        // Show Error
+        const err = await getResponse(e as Response);
+        dispatch(serverErrorAlertAdded(err));
       }
     },
     [clusterName]
@@ -70,7 +54,7 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
   return (
     <FormProvider {...methods}>
       <PageHeading text="Create new schema" />
-      <NewSchemaFormStyled onSubmit={handleSubmit(onSubmit)}>
+      <S.Form onSubmit={handleSubmit(onSubmit)}>
         <div>
           <InputLabel>Subject *</InputLabel>
           <Input
@@ -132,7 +116,7 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
         >
           Submit
         </Button>
-      </NewSchemaFormStyled>
+      </S.Form>
     </FormProvider>
   );
 };

+ 0 - 10
kafka-ui-react-app/src/components/Schemas/New/NewContainer.ts

@@ -1,10 +0,0 @@
-import { connect } from 'react-redux';
-import { createSchema } from 'redux/actions';
-
-import New from './New';
-
-const mapDispatchToProps = {
-  createSchema,
-};
-
-export default connect(null, mapDispatchToProps)(New);

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

@@ -1,41 +1,25 @@
 import React from 'react';
-import { store } from 'redux/store';
-import { mount, shallow } from 'enzyme';
-import { Provider } from 'react-redux';
-import { StaticRouter } from 'react-router-dom';
-import NewContainer from 'components/Schemas/New/NewContainer';
-import New, { NewProps } from 'components/Schemas/New/New';
-import { ThemeProvider } from 'styled-components';
-import theme from 'theme/theme';
+import New from 'components/Schemas/New/New';
+import { render } from 'lib/testHelpers';
+import { clusterSchemaNewPath } from 'lib/paths';
+import { Route } from 'react-router';
+import { screen } from '@testing-library/dom';
 
-describe('New', () => {
-  describe('Container', () => {
-    it('renders view', () => {
-      const component = shallow(
-        <ThemeProvider theme={theme}>
-          <Provider store={store}>
-            <NewContainer />
-          </Provider>
-        </ThemeProvider>
-      );
+const clusterName = 'local';
 
-      expect(component.exists()).toBeTruthy();
-    });
-  });
-
-  describe('View', () => {
-    const pathname = '/ui/clusters/clusterName/schemas/create-new';
-
-    const setupWrapper = (props: Partial<NewProps> = {}) => (
-      <ThemeProvider theme={theme}>
-        <StaticRouter location={{ pathname }} context={{}}>
-          <New createSchema={jest.fn()} {...props} />
-        </StaticRouter>
-      </ThemeProvider>
+describe('New Component', () => {
+  beforeEach(() => {
+    render(
+      <Route path={clusterSchemaNewPath(':clusterName')}>
+        <New />
+      </Route>,
+      {
+        pathname: clusterSchemaNewPath(clusterName),
+      }
     );
+  });
 
-    it('matches snapshot', () => {
-      expect(mount(setupWrapper())).toMatchSnapshot();
-    });
+  it('renders component', () => {
+    expect(screen.getByText('Create new schema')).toBeInTheDocument();
   });
 });

+ 0 - 1137
kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap

@@ -1,1137 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`New View matches snapshot 1`] = `
-.c2 {
-  font-weight: 500;
-  font-size: 12px;
-  line-height: 20px;
-  color: #454F54;
-}
-
-.c4 {
-  border: 1px #ABB5BA solid;
-  border-radius: 4px;
-  height: 32px;
-  width: 100%;
-  padding-left: 12px;
-  font-size: 14px;
-}
-
-.c4::-webkit-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c4::-moz-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c4:-ms-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c4::placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c4:hover {
-  border-color: #73848C;
-}
-
-.c4:focus {
-  outline: none;
-  border-color: #454F54;
-}
-
-.c4:focus::-webkit-input-placeholder {
-  color: transparent;
-}
-
-.c4:focus::-moz-placeholder {
-  color: transparent;
-}
-
-.c4:focus:-ms-input-placeholder {
-  color: transparent;
-}
-
-.c4:focus::placeholder {
-  color: transparent;
-}
-
-.c4:disabled {
-  color: #ABB5BA;
-  border-color: #E3E6E8;
-  cursor: not-allowed;
-}
-
-.c4:read-only {
-  color: #171A1C;
-  border: none;
-  background-color: #F1F2F3;
-  cursor: not-allowed;
-}
-
-.c4:-moz-read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-.c4:read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-.c5 {
-  color: #E51A1A;
-  font-size: 12px;
-}
-
-.c3 {
-  position: relative;
-}
-
-.c8 {
-  height: 32px;
-  border: 1px #ABB5BA solid;
-  border-radius: 4px;
-  font-size: 14px;
-  width: 100%;
-  padding-left: 12px;
-  padding-right: 16px;
-  color: #171A1C;
-  min-width: auto;
-  background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
-  background-repeat: no-repeat !important;
-  background-position-x: calc(100% - 8px) !important;
-  background-position-y: 55% !important;
-  -webkit-appearance: none !important;
-  -moz-appearance: none !important;
-  appearance: none !important;
-}
-
-.c8:hover {
-  color: #171A1C;
-  border-color: #73848C;
-}
-
-.c8:focus {
-  outline: none;
-  color: #171A1C;
-  border-color: #454F54;
-}
-
-.c8:disabled {
-  color: #ABB5BA;
-  border-color: #E3E6E8;
-  cursor: not-allowed;
-}
-
-.c7 {
-  position: relative;
-}
-
-.c9 {
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-flex-direction: row;
-  -ms-flex-direction: row;
-  flex-direction: row;
-  -webkit-align-items: center;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
-  align-items: center;
-  -webkit-box-pack: center;
-  -webkit-justify-content: center;
-  -ms-flex-pack: center;
-  justify-content: center;
-  padding: 0px 12px;
-  border: none;
-  border-radius: 4px;
-  white-space: nowrap;
-  background: #4F4FFF;
-  color: #FFFFFF;
-  font-size: 14px;
-  height: 32px;
-}
-
-.c9:hover:enabled {
-  background: #1717CF;
-  color: #FFFFFF;
-  cursor: pointer;
-}
-
-.c9:active:enabled {
-  background: #1414B8;
-  color: #FFFFFF;
-}
-
-.c9:disabled {
-  opacity: 0.5;
-  cursor: not-allowed;
-}
-
-.c9 a {
-  color: white;
-}
-
-.c9 i {
-  margin-right: 7px;
-}
-
-.c6 {
-  border: 1px #ABB5BA solid;
-  border-radius: 4px;
-  width: 100%;
-  padding: 12px;
-  padding-top: 6px;
-}
-
-.c6::-webkit-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c6::-moz-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c6:-ms-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c6::placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c6:hover {
-  border-color: #73848C;
-}
-
-.c6:focus {
-  outline: none;
-  border-color: #454F54;
-}
-
-.c6:focus::-webkit-input-placeholder {
-  color: transparent;
-}
-
-.c6:focus::-moz-placeholder {
-  color: transparent;
-}
-
-.c6:focus:-ms-input-placeholder {
-  color: transparent;
-}
-
-.c6:focus::placeholder {
-  color: transparent;
-}
-
-.c6:disabled {
-  color: #ABB5BA;
-  border-color: #E3E6E8;
-  cursor: not-allowed;
-}
-
-.c6:read-only {
-  color: #171A1C;
-  border: none;
-  background-color: #F1F2F3;
-  cursor: not-allowed;
-}
-
-.c6:-moz-read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-.c6:read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-.c0 {
-  height: 56px;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-pack: justify;
-  -webkit-justify-content: space-between;
-  -ms-flex-pack: justify;
-  justify-content: space-between;
-  -webkit-align-items: center;
-  -webkit-box-align: center;
-  -ms-flex-align: center;
-  align-items: center;
-  padding: 0px 16px;
-}
-
-.c0 h1 {
-  font-size: 24px;
-  font-weight: 500;
-  line-height: 32px;
-  color: #000;
-}
-
-.c0 > div {
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  gap: 16px;
-}
-
-.c1 {
-  padding: 16px;
-  padding-top: 0;
-  display: -webkit-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-flex-direction: column;
-  -ms-flex-direction: column;
-  flex-direction: column;
-  gap: 16px;
-}
-
-.c1 > button:last-child {
-  -webkit-align-self: flex-start;
-  -ms-flex-item-align: start;
-  align-self: flex-start;
-}
-
-.c1 textarea {
-  height: 200px;
-}
-
-.c1 select {
-  width: 30%;
-}
-
-<Component
-  theme={
-    Object {
-      "alert": Object {
-        "color": Object {
-          "error": "#FAD1D1",
-          "info": "#E3E6E8",
-          "success": "#D6F5E0",
-          "warning": "#FFEECC",
-        },
-      },
-      "buttonStyles": Object {
-        "fontSize": Object {
-          "L": "16px",
-          "M": "14px",
-          "S": "14px",
-        },
-        "height": Object {
-          "L": "40px",
-          "M": "32px",
-          "S": "24px",
-        },
-        "primary": Object {
-          "backgroundColor": Object {
-            "active": "#1414B8",
-            "hover": "#1717CF",
-            "normal": "#4F4FFF",
-          },
-          "color": "#FFFFFF",
-          "invertedColors": Object {
-            "active": "#1414B8",
-            "hover": "#1717CF",
-            "normal": "#4F4FFF",
-          },
-        },
-        "secondary": Object {
-          "backgroundColor": Object {
-            "active": "#D5DADD",
-            "hover": "#E3E6E8",
-            "normal": "#F1F2F3",
-          },
-          "color": "#171A1C",
-          "invertedColors": Object {
-            "active": "#171A1C",
-            "hover": "#454F54",
-            "normal": "#73848C",
-          },
-        },
-      },
-      "circularAlert": Object {
-        "color": Object {
-          "error": "#E51A1A",
-          "info": "#E3E6E8",
-          "success": "#5CD685",
-          "warning": "#FFEECC",
-        },
-      },
-      "layout": Object {
-        "minWidth": "1200px",
-        "navBarHeight": "3.25rem",
-        "navBarWidth": "201px",
-      },
-      "menuStyles": Object {
-        "backgroundColor": Object {
-          "active": "#E3E6E8",
-          "hover": "#F1F2F3",
-          "normal": "#FFFFFF",
-        },
-        "chevronIconColor": "#73848C",
-        "color": Object {
-          "active": "#171A1C",
-          "hover": "#73848C",
-          "normal": "#73848C",
-        },
-        "statusIconColor": Object {
-          "offline": "#E51A1A",
-          "online": "#5CD685",
-        },
-      },
-      "metrics": Object {
-        "backgroundColor": "#F1F2F3",
-        "indicator": Object {
-          "backgroundColor": "#FFFFFF",
-          "lightTextColor": "#ABB5BA",
-          "titleColor": "#73848C",
-          "warningTextColor": "#E51A1A",
-        },
-      },
-      "pageLoader": Object {
-        "borderBottomColor": "#FFFFFF",
-        "borderColor": "#4F4FFF",
-      },
-      "paginationStyles": Object {
-        "borderColor": Object {
-          "active": "#454F54",
-          "disabled": "#C7CED1",
-          "hover": "#73848C",
-          "normal": "#ABB5BA",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "disabled": "#C7CED1",
-          "hover": "#171A1C",
-          "normal": "#171A1C",
-        },
-      },
-      "primaryTabStyles": Object {
-        "borderColor": Object {
-          "active": "#4F4FFF",
-          "hover": "transparent",
-          "normal": "transparent",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "hover": "#171A1C",
-          "normal": "#73848C",
-        },
-      },
-      "secondaryTabStyles": Object {
-        "backgroundColor": Object {
-          "active": "#E3E6E8",
-          "hover": "#F1F2F3",
-          "normal": "#FFFFFF",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "hover": "#171A1C",
-          "normal": "#73848C",
-        },
-      },
-      "selectStyles": Object {
-        "borderColor": Object {
-          "active": "#454F54",
-          "disabled": "#E3E6E8",
-          "hover": "#73848C",
-          "normal": "#ABB5BA",
-        },
-        "color": Object {
-          "active": "#171A1C",
-          "disabled": "#ABB5BA",
-          "hover": "#171A1C",
-          "normal": "#171A1C",
-        },
-      },
-      "switch": Object {
-        "checked": "#29A352",
-        "unchecked": "#ABB5BA",
-      },
-      "tagStyles": Object {
-        "backgroundColor": Object {
-          "gray": "#E3E6E8",
-          "green": "#D6F5E0",
-          "red": "#FAD1D1",
-          "white": "#E3E6E8",
-          "yellow": "#FFEECC",
-        },
-        "color": "#171A1C",
-      },
-      "thStyles": Object {
-        "backgroundColor": Object {
-          "normal": "#FFFFFF",
-        },
-        "color": Object {
-          "normal": "#73848C",
-        },
-        "previewColor": Object {
-          "normal": "#4F4FFF",
-        },
-      },
-    }
-  }
->
-  <StaticRouter
-    context={Object {}}
-    location={
-      Object {
-        "pathname": "/ui/clusters/clusterName/schemas/create-new",
-      }
-    }
-  >
-    <Router
-      history={
-        Object {
-          "action": "POP",
-          "block": [Function],
-          "createHref": [Function],
-          "go": [Function],
-          "goBack": [Function],
-          "goForward": [Function],
-          "listen": [Function],
-          "location": Object {
-            "hash": "",
-            "pathname": "/ui/clusters/clusterName/schemas/create-new",
-            "search": "",
-          },
-          "push": [Function],
-          "replace": [Function],
-        }
-      }
-      staticContext={Object {}}
-    >
-      <New
-        createSchema={[MockFunction]}
-      >
-        <Component
-          clearErrors={[Function]}
-          control={
-            Object {
-              "controllerSubjectRef": Object {
-                "current": Re {
-                  "observers": Array [],
-                },
-              },
-              "defaultValuesRef": Object {
-                "current": Object {},
-              },
-              "fieldArrayDefaultValuesRef": Object {
-                "current": Object {},
-              },
-              "fieldArrayNamesRef": Object {
-                "current": Set {},
-              },
-              "fieldArraySubjectRef": Object {
-                "current": Re {
-                  "observers": Array [
-                    ke {
-                      "closed": false,
-                      "observer": Object {
-                        "next": [Function],
-                      },
-                    },
-                  ],
-                },
-              },
-              "fieldsRef": Object {
-                "current": Object {
-                  "schema": Object {
-                    "_f": Object {
-                      "mount": true,
-                      "name": "schema",
-                      "ref": .c0 {
-  border: 1px #ABB5BA solid;
-  border-radius: 4px;
-  width: 100%;
-  padding: 12px;
-  padding-top: 6px;
-}
-
-.c0::-webkit-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0::-moz-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0:-ms-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0::placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0:hover {
-  border-color: #73848C;
-}
-
-.c0:focus {
-  outline: none;
-  border-color: #454F54;
-}
-
-.c0:focus::-webkit-input-placeholder {
-  color: transparent;
-}
-
-.c0:focus::-moz-placeholder {
-  color: transparent;
-}
-
-.c0:focus:-ms-input-placeholder {
-  color: transparent;
-}
-
-.c0:focus::placeholder {
-  color: transparent;
-}
-
-.c0:disabled {
-  color: #ABB5BA;
-  border-color: #E3E6E8;
-  cursor: not-allowed;
-}
-
-.c0:read-only {
-  color: #171A1C;
-  border: none;
-  background-color: #F1F2F3;
-  cursor: not-allowed;
-}
-
-.c0:-moz-read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-.c0:read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-<textarea
-                        class="c0"
-                        name="schema"
-                      />,
-                      "required": "Schema is required.",
-                      "value": "",
-                    },
-                  },
-                  "schemaType": Object {
-                    "_f": Object {
-                      "mount": true,
-                      "name": "schemaType",
-                      "ref": .c0 {
-  height: 32px;
-  border: 1px #ABB5BA solid;
-  border-radius: 4px;
-  font-size: 14px;
-  width: 100%;
-  padding-left: 12px;
-  padding-right: 16px;
-  color: #171A1C;
-  min-width: auto;
-  background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
-  background-repeat: no-repeat !important;
-  background-position-x: calc(100% - 8px) !important;
-  background-position-y: 55% !important;
-  -webkit-appearance: none !important;
-  -moz-appearance: none !important;
-  appearance: none !important;
-}
-
-.c0:hover {
-  color: #171A1C;
-  border-color: #73848C;
-}
-
-.c0:focus {
-  outline: none;
-  color: #171A1C;
-  border-color: #454F54;
-}
-
-.c0:disabled {
-  color: #ABB5BA;
-  border-color: #E3E6E8;
-  cursor: not-allowed;
-}
-
-<select
-                        class="c0"
-                        name="schemaType"
-                      >
-                        <option
-                          value="AVRO"
-                        >
-                          AVRO
-                        </option>
-                        <option
-                          value="JSON"
-                        >
-                          JSON
-                        </option>
-                        <option
-                          value="PROTOBUF"
-                        >
-                          PROTOBUF
-                        </option>
-                      </select>,
-                      "required": "Schema Type is required.",
-                      "value": "AVRO",
-                    },
-                  },
-                  "subject": Object {
-                    "_f": Object {
-                      "mount": true,
-                      "name": "subject",
-                      "pattern": Object {
-                        "message": "Only alphanumeric, _, -, and . allowed",
-                        "value": /\\^\\[\\.,A-Za-z0-9_-\\]\\+\\$/,
-                      },
-                      "ref": .c0 {
-  border: 1px #ABB5BA solid;
-  border-radius: 4px;
-  height: 32px;
-  width: 100%;
-  padding-left: 12px;
-  font-size: 14px;
-}
-
-.c0::-webkit-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0::-moz-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0:-ms-input-placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0::placeholder {
-  color: #ABB5BA;
-  font-size: 14px;
-}
-
-.c0:hover {
-  border-color: #73848C;
-}
-
-.c0:focus {
-  outline: none;
-  border-color: #454F54;
-}
-
-.c0:focus::-webkit-input-placeholder {
-  color: transparent;
-}
-
-.c0:focus::-moz-placeholder {
-  color: transparent;
-}
-
-.c0:focus:-ms-input-placeholder {
-  color: transparent;
-}
-
-.c0:focus::placeholder {
-  color: transparent;
-}
-
-.c0:disabled {
-  color: #ABB5BA;
-  border-color: #E3E6E8;
-  cursor: not-allowed;
-}
-
-.c0:read-only {
-  color: #171A1C;
-  border: none;
-  background-color: #F1F2F3;
-  cursor: not-allowed;
-}
-
-.c0:-moz-read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-.c0:read-only:focus::placeholder {
-  color: #ABB5BA;
-}
-
-.c1 {
-  position: relative;
-}
-
-<input
-                        autocomplete="off"
-                        class="c0 c1"
-                        name="subject"
-                        placeholder="Schema Name"
-                      />,
-                      "required": "Schema Name is required.",
-                      "value": "",
-                    },
-                  },
-                },
-              },
-              "fieldsWithValidationRef": Object {
-                "current": Object {
-                  "schema": true,
-                  "schemaType": true,
-                  "subject": true,
-                },
-              },
-              "formStateRef": Object {
-                "current": Object {
-                  "dirtyFields": Object {},
-                  "errors": Object {},
-                  "isDirty": false,
-                  "isSubmitSuccessful": false,
-                  "isSubmitted": false,
-                  "isSubmitting": false,
-                  "isValid": false,
-                  "isValidating": false,
-                  "submitCount": 0,
-                  "touchedFields": Object {},
-                },
-              },
-              "formStateSubjectRef": Object {
-                "current": Re {
-                  "observers": Array [
-                    ke {
-                      "closed": false,
-                      "observer": Object {
-                        "next": [Function],
-                      },
-                    },
-                  ],
-                },
-              },
-              "getIsDirty": [Function],
-              "inFieldArrayActionRef": Object {
-                "current": false,
-              },
-              "isWatchAllRef": Object {
-                "current": false,
-              },
-              "readFormStateRef": Object {
-                "current": Object {
-                  "dirtyFields": false,
-                  "errors": "all",
-                  "isDirty": "all",
-                  "isSubmitting": "all",
-                  "isValid": false,
-                  "isValidating": false,
-                  "touchedFields": false,
-                },
-              },
-              "register": [Function],
-              "shouldUnmount": undefined,
-              "unregister": [Function],
-              "validFieldsRef": Object {
-                "current": Object {},
-              },
-              "watchFieldsRef": Object {
-                "current": Set {},
-              },
-              "watchInternal": [Function],
-              "watchSubjectRef": Object {
-                "current": Re {
-                  "observers": Array [],
-                },
-              },
-            }
-          }
-          formState={
-            Object {
-              "dirtyFields": Object {},
-              "errors": Object {},
-              "isDirty": false,
-              "isSubmitSuccessful": false,
-              "isSubmitted": false,
-              "isSubmitting": false,
-              "isValid": false,
-              "isValidating": false,
-              "submitCount": 0,
-              "touchedFields": Object {},
-            }
-          }
-          getValues={[Function]}
-          handleSubmit={[Function]}
-          register={[Function]}
-          reset={[Function]}
-          setError={[Function]}
-          setFocus={[Function]}
-          setValue={[Function]}
-          trigger={[Function]}
-          unregister={[Function]}
-          watch={[Function]}
-        >
-          <Styled(PageHeading)
-            text="Create new schema"
-          >
-            <PageHeading
-              className="c0"
-              text="Create new schema"
-            >
-              <div
-                className="c0"
-              >
-                <h1>
-                  Create new schema
-                </h1>
-                <div />
-              </div>
-            </PageHeading>
-          </Styled(PageHeading)>
-          <styled.form
-            onSubmit={[Function]}
-          >
-            <form
-              className="c1"
-              onSubmit={[Function]}
-            >
-              <div>
-                <styled.label>
-                  <label
-                    className="c2"
-                  >
-                    Subject *
-                  </label>
-                </styled.label>
-                <Styled(Input)
-                  autoComplete="off"
-                  disabled={false}
-                  hookFormOptions={
-                    Object {
-                      "pattern": Object {
-                        "message": "Only alphanumeric, _, -, and . allowed",
-                        "value": /\\^\\[\\.,A-Za-z0-9_-\\]\\+\\$/,
-                      },
-                      "required": "Schema Name is required.",
-                    }
-                  }
-                  inputSize="M"
-                  name="subject"
-                  placeholder="Schema Name"
-                >
-                  <Input
-                    autoComplete="off"
-                    className="c3"
-                    disabled={false}
-                    hookFormOptions={
-                      Object {
-                        "pattern": Object {
-                          "message": "Only alphanumeric, _, -, and . allowed",
-                          "value": /\\^\\[\\.,A-Za-z0-9_-\\]\\+\\$/,
-                        },
-                        "required": "Schema Name is required.",
-                      }
-                    }
-                    inputSize="M"
-                    name="subject"
-                    placeholder="Schema Name"
-                  >
-                    <div
-                      className="c3"
-                    >
-                      <styled.input
-                        autoComplete="off"
-                        className="c3"
-                        disabled={false}
-                        hasLeftIcon={false}
-                        inputSize="M"
-                        name="subject"
-                        onBlur={[Function]}
-                        onChange={[Function]}
-                        placeholder="Schema Name"
-                      >
-                        <input
-                          autoComplete="off"
-                          className="c4 c3"
-                          disabled={false}
-                          name="subject"
-                          onBlur={[Function]}
-                          onChange={[Function]}
-                          placeholder="Schema Name"
-                        />
-                      </styled.input>
-                    </div>
-                  </Input>
-                </Styled(Input)>
-                <styled.p>
-                  <p
-                    className="c5"
-                  >
-                    <Component
-                      errors={Object {}}
-                      name="subject"
-                    />
-                  </p>
-                </styled.p>
-              </div>
-              <div>
-                <styled.label>
-                  <label
-                    className="c2"
-                  >
-                    Schema *
-                  </label>
-                </styled.label>
-                <styled.textarea
-                  disabled={false}
-                  name="schema"
-                  onBlur={[Function]}
-                  onChange={[Function]}
-                >
-                  <textarea
-                    className="c6"
-                    disabled={false}
-                    name="schema"
-                    onBlur={[Function]}
-                    onChange={[Function]}
-                  />
-                </styled.textarea>
-                <styled.p>
-                  <p
-                    className="c5"
-                  >
-                    <Component
-                      errors={Object {}}
-                      name="schema"
-                    />
-                  </p>
-                </styled.p>
-              </div>
-              <div>
-                <styled.label>
-                  <label
-                    className="c2"
-                  >
-                    Schema Type *
-                  </label>
-                </styled.label>
-                <Styled(Select)
-                  disabled={false}
-                  hookFormOptions={
-                    Object {
-                      "required": "Schema Type is required.",
-                    }
-                  }
-                  name="schemaType"
-                  selectSize="M"
-                >
-                  <Select
-                    className="c7"
-                    disabled={false}
-                    hookFormOptions={
-                      Object {
-                        "required": "Schema Type is required.",
-                      }
-                    }
-                    name="schemaType"
-                    selectSize="M"
-                  >
-                    <div
-                      className="select-wrapper c7"
-                    >
-                      <styled.select
-                        disabled={false}
-                        name="schemaType"
-                        onBlur={[Function]}
-                        onChange={[Function]}
-                        selectSize="M"
-                      >
-                        <select
-                          className="c8"
-                          disabled={false}
-                          name="schemaType"
-                          onBlur={[Function]}
-                          onChange={[Function]}
-                        >
-                          <option
-                            value="AVRO"
-                          >
-                            AVRO
-                          </option>
-                          <option
-                            value="JSON"
-                          >
-                            JSON
-                          </option>
-                          <option
-                            value="PROTOBUF"
-                          >
-                            PROTOBUF
-                          </option>
-                        </select>
-                      </styled.select>
-                    </div>
-                  </Select>
-                </Styled(Select)>
-                <styled.p>
-                  <p
-                    className="c5"
-                  >
-                    <Component
-                      errors={Object {}}
-                      name="schemaType"
-                    />
-                  </p>
-                </styled.p>
-              </div>
-              <Button
-                buttonSize="M"
-                buttonType="primary"
-                disabled={true}
-                type="submit"
-              >
-                <styled.button
-                  buttonSize="M"
-                  buttonType="primary"
-                  disabled={true}
-                  type="submit"
-                >
-                  <button
-                    className="c9"
-                    disabled={true}
-                    type="submit"
-                  >
-                    Submit
-                  </button>
-                </styled.button>
-              </Button>
-            </form>
-          </styled.form>
-        </Component>
-      </New>
-    </Router>
-  </StaticRouter>
-</Component>
-`;

+ 45 - 29
kafka-ui-react-app/src/components/Schemas/Schemas.tsx

@@ -1,39 +1,55 @@
 import React from 'react';
-import { Switch, Route } from 'react-router-dom';
+import { Switch, Route, useParams } from 'react-router-dom';
 import {
   clusterSchemaNewPath,
   clusterSchemaPath,
+  clusterSchemaEditPath,
   clusterSchemasPath,
 } from 'lib/paths';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import {
+  fetchSchemas,
+  getAreSchemasFulfilled,
+} from 'redux/reducers/schemas/schemasSlice';
+import PageLoader from 'components/common/PageLoader/PageLoader';
+import List from 'components/Schemas/List/List';
+import Details from 'components/Schemas/Details/Details';
+import New from 'components/Schemas/New/New';
+import Edit from 'components/Schemas/Edit/Edit';
+
+const Schemas: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const { clusterName } = useParams<{ clusterName: string }>();
+  const isFetched = useAppSelector(getAreSchemasFulfilled);
+
+  React.useEffect(() => {
+    dispatch(fetchSchemas(clusterName));
+  }, []);
 
-import ListContainer from './List/ListContainer';
-import DetailsContainer from './Details/DetailsContainer';
-import NewContainer from './New/NewContainer';
-import EditContainer from './Edit/EditContainer';
+  if (!isFetched) {
+    return <PageLoader />;
+  }
 
-const Schemas: React.FC = () => (
-  <Switch>
-    <Route
-      exact
-      path={clusterSchemasPath(':clusterName')}
-      component={ListContainer}
-    />
-    <Route
-      exact
-      path={clusterSchemaNewPath(':clusterName')}
-      component={NewContainer}
-    />
-    <Route
-      exact
-      path={clusterSchemaPath(':clusterName', ':subject')}
-      component={DetailsContainer}
-    />
-    <Route
-      exact
-      path="/ui/clusters/:clusterName/schemas/:subject/edit"
-      component={EditContainer}
-    />
-  </Switch>
-);
+  return (
+    <Switch>
+      <Route exact path={clusterSchemasPath(':clusterName')} component={List} />
+      <Route
+        exact
+        path={clusterSchemaNewPath(':clusterName')}
+        component={New}
+      />
+      <Route
+        exact
+        path={clusterSchemaPath(':clusterName', ':subject')}
+        component={Details}
+      />
+      <Route
+        exact
+        path={clusterSchemaEditPath(':clusterName', ':subject')}
+        component={Edit}
+      />
+    </Switch>
+  );
+};
 
 export default Schemas;

+ 47 - 11
kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx

@@ -1,18 +1,54 @@
 import React from 'react';
-import { shallow } from 'enzyme';
-import { StaticRouter } from 'react-router-dom';
 import Schemas from 'components/Schemas/Schemas';
+import { render } from 'lib/testHelpers';
+import {
+  clusterPath,
+  clusterSchemaEditPath,
+  clusterSchemaNewPath,
+  clusterSchemaPath,
+  clusterSchemasPath,
+} from 'lib/paths';
+import { screen, waitFor } from '@testing-library/dom';
+import { Route } from 'react-router';
+import fetchMock from 'fetch-mock';
+import { schemaVersion } from 'redux/reducers/schemas/__test__/fixtures';
 
-describe('Schemas', () => {
-  const pathname = `/ui/clusters/clusterName/schemas`;
+const renderComponent = (pathname: string) =>
+  render(
+    <Route path={clusterPath(':clusterName')}>
+      <Schemas />
+    </Route>,
+    { pathname }
+  );
 
-  it('renders', () => {
-    const wrapper = shallow(
-      <StaticRouter location={{ pathname }} context={{}}>
-        <Schemas />
-      </StaticRouter>
-    );
+const clusterName = 'secondLocal';
+
+jest.mock('components/Schemas/List/List', () => () => <div>List</div>);
+jest.mock('components/Schemas/Details/Details', () => () => <div>Details</div>);
+jest.mock('components/Schemas/New/New', () => () => <div>New</div>);
+jest.mock('components/Schemas/Edit/Edit', () => () => <div>Edit</div>);
 
-    expect(wrapper.exists('Schemas')).toBeTruthy();
+describe('Schemas', () => {
+  beforeEach(() => {
+    fetchMock.getOnce(`/api/clusters/${clusterName}/schemas`, [schemaVersion]);
+  });
+  afterEach(() => fetchMock.restore());
+  it('renders List', async () => {
+    renderComponent(clusterSchemasPath(clusterName));
+    await waitFor(() => expect(screen.queryByText('List')).toBeInTheDocument());
+  });
+  it('renders New', async () => {
+    renderComponent(clusterSchemaNewPath(clusterName));
+    await waitFor(() => expect(screen.queryByText('New')).toBeInTheDocument());
+  });
+  it('renders Details', async () => {
+    renderComponent(clusterSchemaPath(clusterName, schemaVersion.subject));
+    await waitFor(() =>
+      expect(screen.queryByText('Details')).toBeInTheDocument()
+    );
+  });
+  it('renders Edit', async () => {
+    renderComponent(clusterSchemaEditPath(clusterName, schemaVersion.subject));
+    await waitFor(() => expect(screen.queryByText('Edit')).toBeInTheDocument());
   });
 });

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

@@ -2,25 +2,22 @@ import React from 'react';
 import Filters, {
   FiltersProps,
 } from 'components/Topics/Topic/Details/Messages/Filters/Filters';
-import { StaticRouter } from 'react-router';
 import { render } from 'lib/testHelpers';
 
 const setupWrapper = (props?: Partial<FiltersProps>) => (
-  <StaticRouter>
-    <Filters
-      clusterName="test-cluster"
-      topicName="test-topic"
-      partitions={[{ partition: 0, offsetMin: 0, offsetMax: 100 }]}
-      meta={{}}
-      isFetching={false}
-      addMessage={jest.fn()}
-      resetMessages={jest.fn()}
-      updatePhase={jest.fn()}
-      updateMeta={jest.fn()}
-      setIsFetching={jest.fn()}
-      {...props}
-    />
-  </StaticRouter>
+  <Filters
+    clusterName="test-cluster"
+    topicName="test-topic"
+    partitions={[{ partition: 0, offsetMin: 0, offsetMax: 100 }]}
+    meta={{}}
+    isFetching={false}
+    addMessage={jest.fn()}
+    resetMessages={jest.fn()}
+    updatePhase={jest.fn()}
+    updateMeta={jest.fn()}
+    setIsFetching={jest.fn()}
+    {...props}
+  />
 );
 describe('Filters component', () => {
   it('matches the snapshot', () => {

+ 2 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/__snapshots__/Filters.spec.tsx.snap

@@ -280,6 +280,7 @@ exports[`Filters component matches the snapshot 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 
@@ -914,6 +915,7 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
   background: #F1F2F3;
   color: #171A1C;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 

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

@@ -21,26 +21,25 @@ describe('Details', () => {
 
   const setupComponent = (pathname: string) =>
     render(
-      <StaticRouter location={{ pathname }}>
-        <ClusterContext.Provider
-          value={{
-            isReadOnly: false,
-            hasKafkaConnectConfigured: true,
-            hasSchemaRegistryConfigured: true,
-            isTopicDeletionAllowed: true,
-          }}
-        >
-          <Details
-            clusterName={mockClusterName}
-            topicName={internalTopicPayload.name}
-            name={internalTopicPayload.name}
-            isInternal={false}
-            deleteTopic={mockDelete}
-            clearTopicMessages={mockClearTopicMessages}
-            isDeleted={false}
-          />
-        </ClusterContext.Provider>
-      </StaticRouter>
+      <ClusterContext.Provider
+        value={{
+          isReadOnly: false,
+          hasKafkaConnectConfigured: true,
+          hasSchemaRegistryConfigured: true,
+          isTopicDeletionAllowed: true,
+        }}
+      >
+        <Details
+          clusterName={mockClusterName}
+          topicName={internalTopicPayload.name}
+          name={internalTopicPayload.name}
+          isInternal={false}
+          deleteTopic={mockDelete}
+          clearTopicMessages={mockClearTopicMessages}
+          isDeleted={false}
+        />
+      </ClusterContext.Provider>,
+      { pathname }
     );
 
   describe('when it has readonly flag', () => {

+ 21 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/__test__/__snapshots__/Details.spec.tsx.snap

@@ -144,7 +144,14 @@ exports[`Details when it has readonly flag does not render the Action button a T
           "warning": "#FFEECC",
         },
       },
+      "headingStyles": Object {
+        "h3": Object {
+          "color": "#73848C",
+          "fontSize": "14px",
+        },
+      },
       "layout": Object {
+        "mainColor": "#F1F2F3",
         "minWidth": "1200px",
         "navBarHeight": "3.25rem",
         "navBarWidth": "201px",
@@ -180,6 +187,7 @@ exports[`Details when it has readonly flag does not render the Action button a T
         "borderColor": "#4F4FFF",
       },
       "paginationStyles": Object {
+        "backgroundColor": "#FFFFFF",
         "borderColor": Object {
           "active": "#454F54",
           "disabled": "#C7CED1",
@@ -192,7 +200,9 @@ exports[`Details when it has readonly flag does not render the Action button a T
           "hover": "#171A1C",
           "normal": "#171A1C",
         },
+        "currentPage": "#E3E6E8",
       },
+      "panelColor": "#FFFFFF",
       "primaryTabStyles": Object {
         "borderColor": Object {
           "active": "#4F4FFF",
@@ -205,6 +215,16 @@ exports[`Details when it has readonly flag does not render the Action button a T
           "normal": "#73848C",
         },
       },
+      "scrollbar": Object {
+        "thumbColor": Object {
+          "active": "#73848C",
+          "normal": "#FFFFFF",
+        },
+        "trackColor": Object {
+          "active": "#F1F2F3",
+          "normal": "#FFFFFF",
+        },
+      },
       "secondaryTabStyles": Object {
         "backgroundColor": Object {
           "active": "#E3E6E8",
@@ -237,7 +257,7 @@ exports[`Details when it has readonly flag does not render the Action button a T
       },
       "tagStyles": Object {
         "backgroundColor": Object {
-          "gray": "#E3E6E8",
+          "gray": "#F1F2F3",
           "green": "#D6F5E0",
           "red": "#FAD1D1",
           "white": "#E3E6E8",

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

@@ -3,25 +3,21 @@ import DangerZone, {
   Props,
 } from 'components/Topics/Topic/Edit/DangerZone/DangerZone';
 import { screen, waitFor } from '@testing-library/react';
-import { ThemeProvider } from 'styled-components';
-import theme from 'theme/theme';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
 
 const setupWrapper = (props?: Partial<Props>) => (
-  <ThemeProvider theme={theme}>
-    <DangerZone
-      clusterName="testCluster"
-      topicName="testTopic"
-      defaultPartitions={3}
-      defaultReplicationFactor={3}
-      partitionsCountIncreased={false}
-      replicationFactorUpdated={false}
-      updateTopicPartitionsCount={jest.fn()}
-      updateTopicReplicationFactor={jest.fn()}
-      {...props}
-    />
-  </ThemeProvider>
+  <DangerZone
+    clusterName="testCluster"
+    topicName="testTopic"
+    defaultPartitions={3}
+    defaultReplicationFactor={3}
+    partitionsCountIncreased={false}
+    replicationFactorUpdated={false}
+    updateTopicPartitionsCount={jest.fn()}
+    updateTopicReplicationFactor={jest.fn()}
+    {...props}
+  />
 );
 
 describe('DangerZone', () => {

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

@@ -24,6 +24,7 @@ exports[`DangerZone is rendered properly 1`] = `
   background: #4F4FFF;
   color: #FFFFFF;
   font-size: 14px;
+  font-weight: 500;
   height: 32px;
 }
 

+ 2 - 1
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx

@@ -41,7 +41,8 @@ const renderComponent = () => {
       <Route path={clusterTopicSendMessagePath(':clusterName', ':topicName')}>
         <SendMessage />
       </Route>
-    </Router>
+    </Router>,
+    { store }
   );
 };
 

+ 7 - 18
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx

@@ -4,14 +4,15 @@ import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
 import { FieldArrayWithId, useFormContext } from 'react-hook-form';
 import { remove as _remove } from 'lodash';
 import { TopicFormData } from 'redux/interfaces';
-import { TopicFormColumn } from 'components/Topics/shared/Form/TopicForm';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import { FormError } from 'components/common/Input/Input.styled';
 import Select from 'components/common/Select/Select';
 import Input from 'components/common/Input/Input';
 import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
 import CloseIcon from 'components/common/Icons/CloseIcon';
-import styled from 'styled-components';
+import * as C from 'components/Topics/shared/Form/TopicForm.styled';
+
+import * as S from './CustomParams.styled';
 
 interface Props {
   isDisabled: boolean;
@@ -22,16 +23,6 @@ interface Props {
   setExistingFields: React.Dispatch<React.SetStateAction<string[]>>;
 }
 
-const CustomParamDeleteButtonWrapper = styled.div`
-  height: 32px;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  align-self: flex-end;
-  flex-grow: 0.25 !important;
-`;
-
 const CustomParamField: React.FC<Props> = ({
   field,
   isDisabled,
@@ -66,12 +57,11 @@ const CustomParamField: React.FC<Props> = ({
   }, [nameValue]);
 
   return (
-    <TopicFormColumn>
+    <C.Column>
       <>
         <div>
           <InputLabel>Custom Parameter</InputLabel>
           <Select
-            selectSize="M"
             name={`customParams.${index}.name` as const}
             hookFormOptions={{
               required: 'Custom Parameter is required.',
@@ -101,7 +91,6 @@ const CustomParamField: React.FC<Props> = ({
       <div>
         <InputLabel>Value</InputLabel>
         <Input
-          inputSize="M"
           name={`customParams.${index}.value` as const}
           hookFormOptions={{
             required: 'Value is required.',
@@ -116,12 +105,12 @@ const CustomParamField: React.FC<Props> = ({
         </FormError>
       </div>
 
-      <CustomParamDeleteButtonWrapper>
+      <S.DeleteButtonWrapper>
         <IconButtonWrapper onClick={() => remove(index)} aria-hidden>
           <CloseIcon />
         </IconButtonWrapper>
-      </CustomParamDeleteButtonWrapper>
-    </TopicFormColumn>
+      </S.DeleteButtonWrapper>
+    </C.Column>
   );
 };
 

+ 16 - 0
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.styled.ts

@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+export const ParamsWrapper = styled.div`
+  margin-top: 16px;
+  margin-bottom: 16px;
+`;
+
+export const DeleteButtonWrapper = styled.div`
+  height: 32px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  align-self: flex-end;
+  flex-grow: 0.25 !important;
+`;

+ 3 - 8
kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx

@@ -2,9 +2,9 @@ import React from 'react';
 import { TopicConfigByName, TopicFormData } from 'redux/interfaces';
 import { useFieldArray, useFormContext } from 'react-hook-form';
 import { Button } from 'components/common/Button/Button';
-import styled from 'styled-components';
 
 import CustomParamField from './CustomParamField';
+import * as S from './CustomParams.styled';
 
 export const INDEX_PREFIX = 'customParams';
 
@@ -13,11 +13,6 @@ interface Props {
   config?: TopicConfigByName;
 }
 
-const CustomParamsWrapper = styled.div`
-  margin-top: 16px;
-  margin-bottom: 16px;
-`;
-
 const CustomParams: React.FC<Props> = ({ isSubmitting }) => {
   const { control } = useFormContext<TopicFormData>();
   const { fields, append, remove } = useFieldArray({
@@ -33,7 +28,7 @@ const CustomParams: React.FC<Props> = ({ isSubmitting }) => {
   };
 
   return (
-    <CustomParamsWrapper>
+    <S.ParamsWrapper>
       {fields.map((field, idx) => (
         <CustomParamField
           key={field.id}
@@ -56,7 +51,7 @@ const CustomParams: React.FC<Props> = ({ isSubmitting }) => {
           Add Custom Parameter
         </Button>
       </div>
-    </CustomParamsWrapper>
+    </S.ParamsWrapper>
   );
 };
 

+ 0 - 1
kafka-ui-react-app/src/components/Topics/shared/Form/TimeToRetain.tsx

@@ -51,7 +51,6 @@ const TimeToRetain: React.FC<Props> = ({ isSubmitting }) => {
         type="number"
         defaultValue={defaultValue}
         name={name}
-        inputSize="M"
         hookFormOptions={{
           min: { value: -1, message: 'must be greater than or equal to -1' },
         }}

+ 17 - 0
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.styled.ts

@@ -0,0 +1,17 @@
+import styled from 'styled-components';
+
+export const Column = styled.div`
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 16px;
+`;
+
+export const NameField = styled.div`
+  flex-grow: 1;
+`;
+
+export const CustomParamsHeading = styled.h4`
+  font-weight: 500;
+`;

+ 49 - 69
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx

@@ -6,13 +6,13 @@ import { ErrorMessage } from '@hookform/error-message';
 import Select from 'components/common/Select/Select';
 import Input from 'components/common/Input/Input';
 import { Button } from 'components/common/Button/Button';
-import styled from 'styled-components';
 import { InputLabel } from 'components/common/Input/InputLabel.styled';
 import { FormError } from 'components/common/Input/Input.styled';
 import { StyledForm } from 'components/common/Form/Form.styles';
 
 import CustomParamsContainer from './CustomParams/CustomParamsContainer';
 import TimeToRetain from './TimeToRetain';
+import * as S from './TopicForm.styled';
 
 interface Props {
   topicName?: TopicName;
@@ -22,17 +22,6 @@ interface Props {
   onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;
 }
 
-export const TopicFormColumn = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  gap: 16px;
-  margin-bottom: 16px;
-  & > * {
-    flex-grow: 1;
-  }
-`;
-
 const TopicForm: React.FC<Props> = ({
   topicName,
   config,
@@ -48,23 +37,22 @@ const TopicForm: React.FC<Props> = ({
     <StyledForm onSubmit={onSubmit}>
       <fieldset disabled={isSubmitting}>
         <fieldset disabled={isEditing}>
-          <TopicFormColumn>
-            <div>
+          <S.Column>
+            <S.NameField>
               <InputLabel>Topic Name *</InputLabel>
               <Input
                 name="name"
                 placeholder="Topic Name"
                 defaultValue={topicName}
-                inputSize="M"
               />
               <FormError>
                 <ErrorMessage errors={errors} name="name" />
               </FormError>
-            </div>
-          </TopicFormColumn>
+            </S.NameField>
+          </S.Column>
 
           {!isEditing && (
-            <TopicFormColumn>
+            <S.Column>
               <div>
                 <InputLabel>Number of partitions *</InputLabel>
                 <Input
@@ -73,34 +61,29 @@ const TopicForm: React.FC<Props> = ({
                   min="1"
                   defaultValue="1"
                   name="partitions"
-                  inputSize="M"
                 />
                 <FormError>
                   <ErrorMessage errors={errors} name="partitions" />
                 </FormError>
               </div>
-            </TopicFormColumn>
+              <div>
+                <InputLabel>Replication Factor *</InputLabel>
+                <Input
+                  type="number"
+                  placeholder="Replication Factor"
+                  min="1"
+                  defaultValue="1"
+                  name="replicationFactor"
+                />
+                <FormError>
+                  <ErrorMessage errors={errors} name="replicationFactor" />
+                </FormError>
+              </div>
+            </S.Column>
           )}
         </fieldset>
 
-        <TopicFormColumn>
-          {!isEditing && (
-            <div>
-              <InputLabel>Replication Factor *</InputLabel>
-              <Input
-                type="number"
-                placeholder="Replication Factor"
-                min="1"
-                defaultValue="1"
-                name="replicationFactor"
-                inputSize="M"
-              />
-              <FormError>
-                <ErrorMessage errors={errors} name="replicationFactor" />
-              </FormError>
-            </div>
-          )}
-
+        <S.Column>
           <div>
             <InputLabel>Min In Sync Replicas *</InputLabel>
             <Input
@@ -109,35 +92,31 @@ const TopicForm: React.FC<Props> = ({
               min="1"
               defaultValue="1"
               name="minInsyncReplicas"
-              inputSize="M"
             />
             <FormError>
               <ErrorMessage errors={errors} name="minInsyncReplicas" />
             </FormError>
           </div>
-        </TopicFormColumn>
+          <div>
+            <InputLabel>Cleanup policy</InputLabel>
+            <Select defaultValue="delete" name="cleanupPolicy" minWidth="250px">
+              <option value="delete">Delete</option>
+              <option value="compact">Compact</option>
+              <option value="compact,delete">Compact,Delete</option>
+            </Select>
+          </div>
+        </S.Column>
 
         <div>
-          <TopicFormColumn>
-            <div>
-              <InputLabel>Cleanup policy</InputLabel>
-              <Select defaultValue="delete" name="cleanupPolicy" selectSize="M">
-                <option value="delete">Delete</option>
-                <option value="compact">Compact</option>
-                <option value="compact,delete">Compact,Delete</option>
-              </Select>
-            </div>
-          </TopicFormColumn>
-
-          <TopicFormColumn>
+          <S.Column>
             <div>
               <TimeToRetain isSubmitting={isSubmitting} />
             </div>
-          </TopicFormColumn>
-          <TopicFormColumn>
+          </S.Column>
+          <S.Column>
             <div>
               <InputLabel>Max size on disk in GB</InputLabel>
-              <Select defaultValue={-1} name="retentionBytes" selectSize="M">
+              <Select defaultValue={-1} name="retentionBytes">
                 <option value={-1}>Not Set</option>
                 <option value={BYTES_IN_GB}>1 GB</option>
                 <option value={BYTES_IN_GB * 10}>10 GB</option>
@@ -145,23 +124,24 @@ const TopicForm: React.FC<Props> = ({
                 <option value={BYTES_IN_GB * 50}>50 GB</option>
               </Select>
             </div>
-          </TopicFormColumn>
-        </div>
 
-        <div>
-          <InputLabel>Maximum message size in bytes *</InputLabel>
-          <Input
-            type="number"
-            min="1"
-            defaultValue="1000012"
-            name="maxMessageBytes"
-            inputSize="M"
-          />
-          <FormError>
-            <ErrorMessage errors={errors} name="maxMessageBytes" />
-          </FormError>
+            <div>
+              <InputLabel>Maximum message size in bytes *</InputLabel>
+              <Input
+                type="number"
+                min="1"
+                defaultValue="1000012"
+                name="maxMessageBytes"
+              />
+              <FormError>
+                <ErrorMessage errors={errors} name="maxMessageBytes" />
+              </FormError>
+            </div>
+          </S.Column>
         </div>
 
+        <S.CustomParamsHeading>Custom parameters</S.CustomParamsHeading>
+
         <CustomParamsContainer isSubmitting={isSubmitting} config={config} />
 
         <Button type="submit" buttonType="primary" buttonSize="L">

+ 50 - 54
kafka-ui-react-app/src/components/__tests__/App.spec.tsx

@@ -1,66 +1,62 @@
 import React from 'react';
-import { screen, within } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
+import { screen, within, waitFor } from '@testing-library/react';
 import App from 'components/App';
 import { render } from 'lib/testHelpers';
-import { store } from 'redux/store';
-import { fetchClusters } from 'redux/reducers/clusters/clustersSlice';
 import { clustersPayload } from 'redux/reducers/clusters/__test__/fixtures';
 import userEvent from '@testing-library/user-event';
+import fetchMock from 'fetch-mock';
 
 describe('App', () => {
-  beforeEach(() => {
-    render(
-      <BrowserRouter basename={window.basePath || '/'}>
-        <App />
-      </BrowserRouter>
-    );
-  });
-
-  it('shows PageLoader until clusters are fulfilled', () => {
-    expect(screen.getByText('Dashboard')).toBeInTheDocument();
-    expect(screen.getByRole('progressbar')).toBeInTheDocument();
-  });
-
-  it('shows Cluster list', () => {
-    store.dispatch({
-      type: fetchClusters.fulfilled.type,
-      payload: clustersPayload,
+  describe('initial state', () => {
+    beforeEach(() => {
+      render(<App />, {
+        pathname: '/',
+      });
+    });
+    it('shows PageLoader until clusters are fulfilled', () => {
+      expect(screen.getByText('Dashboard')).toBeInTheDocument();
+      expect(screen.getByRole('progressbar')).toBeInTheDocument();
+    });
+    it('correctly renders header', () => {
+      const header = screen.getByLabelText('Page Header');
+      expect(header).toBeInTheDocument();
+      expect(
+        within(header).getByText('UI for Apache Kafka')
+      ).toBeInTheDocument();
+      expect(within(header).getAllByRole('separator').length).toEqual(3);
+      expect(within(header).getByRole('button')).toBeInTheDocument();
+    });
+    it('handle burger click correctly', () => {
+      const header = screen.getByLabelText('Page Header');
+      const burger = within(header).getByRole('button');
+      const sidebar = screen.getByLabelText('Sidebar');
+      const overlay = screen.getByLabelText('Overlay');
+      expect(sidebar).toBeInTheDocument();
+      expect(overlay).toBeInTheDocument();
+      expect(overlay).toHaveStyleRule('visibility: hidden');
+      expect(burger).toHaveStyleRule('display: none');
+      userEvent.click(burger);
+      expect(overlay).toHaveStyleRule('visibility: visible');
     });
-    const menuContainer = screen.getByLabelText('Sidebar Menu');
-    expect(menuContainer).toBeInTheDocument();
-    expect(within(menuContainer).getByText('Dashboard')).toBeInTheDocument();
-    expect(
-      within(menuContainer).getByText(clustersPayload[0].name)
-    ).toBeInTheDocument();
-    expect(
-      within(menuContainer).getByText(clustersPayload[1].name)
-    ).toBeInTheDocument();
-
-    expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
-  });
-
-  it('correctly renders header', () => {
-    const header = screen.getByLabelText('Page Header');
-    expect(header).toBeInTheDocument();
-
-    expect(within(header).getByText('UI for Apache Kafka')).toBeInTheDocument();
-    expect(within(header).getAllByRole('separator').length).toEqual(3);
-    expect(within(header).getByRole('button')).toBeInTheDocument();
   });
 
-  it('handle burger click correctly', () => {
-    const header = screen.getByLabelText('Page Header');
-    const burger = within(header).getByRole('button');
-    const sidebar = screen.getByLabelText('Sidebar');
-    const overlay = screen.getByLabelText('Overlay');
-
-    expect(sidebar).toBeInTheDocument();
-    expect(overlay).toBeInTheDocument();
-    expect(overlay).toHaveStyleRule('visibility: hidden');
-    expect(burger).toHaveStyleRule('display: none');
-
-    userEvent.click(burger);
-    expect(overlay).toHaveStyleRule('visibility: visible');
+  describe('with clusters list fetched', () => {
+    it('shows Cluster list', async () => {
+      const mock = fetchMock.getOnce('/api/clusters', clustersPayload);
+      render(<App />, {
+        pathname: '/',
+      });
+      await waitFor(() => expect(mock.called()).toBeTruthy());
+      const menuContainer = screen.getByLabelText('Sidebar Menu');
+      expect(menuContainer).toBeInTheDocument();
+      expect(within(menuContainer).getByText('Dashboard')).toBeInTheDocument();
+      expect(
+        within(menuContainer).getByText(clustersPayload[0].name)
+      ).toBeInTheDocument();
+      expect(
+        within(menuContainer).getByText(clustersPayload[1].name)
+      ).toBeInTheDocument();
+      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
+    });
   });
 });

+ 1 - 6
kafka-ui-react-app/src/components/common/Breadcrumb/__tests__/Breadcrumb.spec.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import { StaticRouter } from 'react-router-dom';
 import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { screen } from '@testing-library/react';
 import { render } from 'lib/testHelpers';
@@ -9,11 +8,7 @@ const createTopicPath = '/ui/clusters/local/topics/create-new';
 
 describe('Breadcrumb component', () => {
   const setupComponent = (pathname: string) =>
-    render(
-      <StaticRouter location={{ pathname }}>
-        <Breadcrumb />
-      </StaticRouter>
-    );
+    render(<Breadcrumb />, { pathname });
 
   it('renders the name of brokers path', () => {
     setupComponent(brokersPath);

+ 1 - 0
kafka-ui-react-app/src/components/common/Button/Button.styled.ts

@@ -25,6 +25,7 @@ const StyledButton = styled.button<ButtonProps>`
       ? props.theme.buttonStyles[props.buttonType].invertedColors.normal
       : props.theme.buttonStyles[props.buttonType].color};
   font-size: ${(props) => props.theme.buttonStyles.fontSize[props.buttonSize]};
+  font-weight: 500;
   height: ${(props) => props.theme.buttonStyles.height[props.buttonSize]};
 
   &:hover:enabled {

+ 1 - 5
kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx

@@ -1,13 +1,9 @@
 import Input, { InputProps } from 'components/common/Input/Input';
-import { ThemeProvider } from 'styled-components';
-import theme from 'theme/theme';
 import React from 'react';
 import { render } from 'lib/testHelpers';
 
 const setupWrapper = (props?: Partial<InputProps>) => (
-  <ThemeProvider theme={theme}>
-    <Input name="test" {...props} />
-  </ThemeProvider>
+  <Input name="test" {...props} />
 );
 jest.mock('react-hook-form', () => ({
   useFormContext: () => ({

+ 9 - 8
kafka-ui-react-app/src/components/common/Pagination/PageControl.tsx

@@ -1,6 +1,6 @@
-import cx from 'classnames';
 import React from 'react';
-import { Link } from 'react-router-dom';
+
+import { PaginationLink } from './Pagination.styled';
 
 export interface PageControlProps {
   current: boolean;
@@ -9,15 +9,16 @@ export interface PageControlProps {
 }
 
 const PageControl: React.FC<PageControlProps> = ({ current, url, page }) => {
-  const classNames = cx('pagination-link', {
-    'is-current': current,
-  });
-
   return (
     <li>
-      <Link className={classNames} to={url} aria-label={`Goto page ${page}`}>
+      <PaginationLink
+        to={url}
+        aria-label={`Goto page ${page}`}
+        $isCurrent={current}
+        role="button"
+      >
         {page}
-      </Link>
+      </PaginationLink>
     </li>
   );
 };

+ 71 - 39
kafka-ui-react-app/src/components/common/Pagination/Pagination.styled.ts

@@ -1,55 +1,87 @@
 import styled from 'styled-components';
-import { Colors } from 'theme/theme';
+import { Link } from 'react-router-dom';
+import theme from 'theme/theme';
 
 export const Wrapper = styled.nav`
   display: flex;
   align-items: flex-end;
-  padding: 0 16px;
+  padding: 25px 16px;
   gap: 15px;
-  padding-top: 25px;
 
   & > ul {
     display: flex;
     align-items: flex-end;
-    & .pagination-link {
-      height: 32px;
-      &.is-current {
-        background-color: ${Colors.brand[50]};
-        border-color: ${Colors.brand[50]};
-      }
+
+    & > li:not(:last-child) {
+      margin-right: 12px;
     }
   }
+`;
 
-  & .pagination-btn {
-    height: 32px;
-    border: 1px solid;
-    background-color: ${Colors.neutral[0]};
-    ${(props) => props.theme.paginationStyles.borderColor.normal};
-    border-radius: 4px;
-    text-align: center;
-    vertical-align: middle;
-    color: ${(props) => props.theme.paginationStyles.color.normal};
+export const PaginationLink = styled(Link)<{ $isCurrent: boolean }>`
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
-    display: flex;
-    align-items: center;
-    padding: 0 12px;
-
-    &:hover {
-      border: 1px solid
-        ${(props) => props.theme.paginationStyles.borderColor.hover};
-      color: ${(props) => props.theme.paginationStyles.color.hover};
-      cursor: pointer;
-    }
-    &:active {
-      border: 1px solid
-        ${(props) => props.theme.paginationStyles.borderColor.active};
-      color: ${(props) => props.theme.paginationStyles.color.active};
-    }
-    &:disabled {
-      border: 1px solid
-        ${(props) => props.theme.paginationStyles.borderColor.disabled};
-      color: ${(props) => props.theme.paginationStyles.color.disabled};
-      cursor: not-allowed;
-    }
+  height: 32px;
+  width: 33px;
+
+  border-radius: 4px;
+  border: 1px solid
+    ${({ $isCurrent }) =>
+      $isCurrent
+        ? theme.paginationStyles.currentPage
+        : theme.paginationStyles.borderColor.normal};
+  background-color: ${({ $isCurrent }) =>
+    $isCurrent
+      ? theme.paginationStyles.currentPage
+      : theme.paginationStyles.backgroundColor};
+  color: ${theme.paginationStyles.color.normal};
+
+  &:hover {
+    border: 1px solid
+      ${({ $isCurrent }) =>
+        $isCurrent
+          ? theme.paginationStyles.currentPage
+          : theme.paginationStyles.borderColor.hover};
+    color: ${(props) => props.theme.paginationStyles.color.hover};
+    cursor: ${({ $isCurrent }) => ($isCurrent ? 'default' : 'pointer')};
   }
 `;
+
+export const PaginationButton = styled(Link)`
+  display: flex;
+  align-items: center;
+  padding: 6px 12px;
+  height: 32px;
+  border: 1px solid ${theme.paginationStyles.borderColor.normal};
+  border-radius: 4px;
+  color: ${theme.paginationStyles.color.normal};
+
+  &:hover {
+    border: 1px solid ${theme.paginationStyles.borderColor.hover};
+    color: ${theme.paginationStyles.color.hover};
+    cursor: pointer;
+  }
+  &:active {
+    border: 1px solid ${theme.paginationStyles.borderColor.active};
+    color: ${theme.paginationStyles.color.active};
+  }
+  &:disabled {
+    border: 1px solid ${theme.paginationStyles.borderColor.disabled};
+    color: ${theme.paginationStyles.color.disabled};
+    cursor: not-allowed;
+  }
+`;
+
+export const DisabledButton = styled.button`
+  display: flex;
+  align-items: center;
+  padding: 6px 12px;
+  height: 32px;
+  border: 1px solid ${theme.paginationStyles.borderColor.disabled};
+  background-color: ${theme.paginationStyles.backgroundColor};
+  border-radius: 4px;
+  font-size: 16px;
+  color: ${theme.paginationStyles.color.disabled};
+`;

+ 9 - 14
kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx

@@ -2,10 +2,9 @@ import { PER_PAGE } from 'lib/constants';
 import usePagination from 'lib/hooks/usePagination';
 import { range } from 'lodash';
 import React from 'react';
-import { Link } from 'react-router-dom';
 import PageControl from 'components/common/Pagination/PageControl';
 
-import { Wrapper } from './Pagination.styled';
+import * as S from './Pagination.styled';
 
 export interface PaginationProps {
   totalPages: number;
@@ -66,15 +65,13 @@ const Pagination: React.FC<PaginationProps> = ({ totalPages }) => {
   }, []);
 
   return (
-    <Wrapper role="navigation" aria-label="pagination">
+    <S.Wrapper role="navigation" aria-label="pagination">
       {currentPage > 1 ? (
-        <Link className="pagination-btn" to={getPath(currentPage - 1)}>
+        <S.PaginationButton to={getPath(currentPage - 1)}>
           Previous
-        </Link>
+        </S.PaginationButton>
       ) : (
-        <button type="button" className="pagination-btn" disabled>
-          Previous
-        </button>
+        <S.DisabledButton disabled>Previous</S.DisabledButton>
       )}
       {totalPages > 1 && (
         <ul>
@@ -113,15 +110,13 @@ const Pagination: React.FC<PaginationProps> = ({ totalPages }) => {
         </ul>
       )}
       {currentPage < totalPages ? (
-        <Link className="pagination-btn" to={getPath(currentPage + 1)}>
+        <S.PaginationButton to={getPath(currentPage + 1)}>
           Next
-        </Link>
+        </S.PaginationButton>
       ) : (
-        <button type="button" className="pagination-btn" disabled>
-          Next
-        </button>
+        <S.DisabledButton disabled>Next</S.DisabledButton>
       )}
-    </Wrapper>
+    </S.Wrapper>
   );
 };
 

+ 15 - 18
kafka-ui-react-app/src/components/common/Pagination/__tests__/PageControl.spec.tsx

@@ -1,36 +1,33 @@
 import React from 'react';
-import { mount, shallow } from 'enzyme';
-import { StaticRouter } from 'react-router';
 import PageControl, {
   PageControlProps,
 } from 'components/common/Pagination/PageControl';
+import { screen } from '@testing-library/react';
+import { render } from 'lib/testHelpers';
+import theme from 'theme/theme';
 
 const page = 138;
 
 describe('PageControl', () => {
-  const setupWrapper = (props: Partial<PageControlProps> = {}) => (
-    <StaticRouter>
-      <PageControl url="/test" page={page} current {...props} />
-    </StaticRouter>
-  );
+  const setupComponent = (props: Partial<PageControlProps> = {}) =>
+    render(<PageControl url="/test" page={page} current {...props} />);
 
   it('renders current page', () => {
-    const wrapper = mount(setupWrapper({ current: true }));
-    expect(wrapper.exists('.pagination-link.is-current')).toBeTruthy();
+    setupComponent({ current: true });
+    expect(screen.getByRole('button')).toHaveStyle(
+      `background-color: ${theme.paginationStyles.currentPage}`
+    );
   });
 
   it('renders non-current page', () => {
-    const wrapper = mount(setupWrapper({ current: false }));
-    expect(wrapper.exists('.pagination-link.is-current')).toBeFalsy();
+    setupComponent({ current: false });
+    expect(screen.getByRole('button')).toHaveStyle(
+      `background-color: ${theme.paginationStyles.backgroundColor}`
+    );
   });
 
   it('renders page number', () => {
-    const wrapper = mount(setupWrapper({ current: false }));
-    expect(wrapper.text()).toEqual(String(page));
-  });
-
-  it('matches snapshot', () => {
-    const wrapper = shallow(<PageControl url="/test" page={page} current />);
-    expect(wrapper).toMatchSnapshot();
+    setupComponent({ current: false });
+    expect(screen.getByRole('button')).toHaveTextContent(String(page));
   });
 });

+ 47 - 44
kafka-ui-react-app/src/components/common/Pagination/__tests__/Pagination.spec.tsx

@@ -1,41 +1,43 @@
 import React from 'react';
-import { mount } from 'enzyme';
 import { StaticRouter } from 'react-router';
 import Pagination, {
   PaginationProps,
 } from 'components/common/Pagination/Pagination';
-import { ThemeProvider } from 'styled-components';
 import theme from 'theme/theme';
+import { render } from 'lib/testHelpers';
+import { screen } from '@testing-library/react';
 
 describe('Pagination', () => {
-  const setupWrapper = (search = '', props: Partial<PaginationProps> = {}) => (
-    <ThemeProvider theme={theme}>
+  const setupComponent = (search = '', props: Partial<PaginationProps> = {}) =>
+    render(
       <StaticRouter location={{ pathname: '/my/test/path/23', search }}>
         <Pagination totalPages={11} {...props} />
       </StaticRouter>
-    </ThemeProvider>
-  );
+    );
 
   describe('next & prev buttons', () => {
     it('renders disable prev button and enabled next link', () => {
-      const wrapper = mount(setupWrapper('?page=1'));
-      expect(wrapper.find('button.pagination-btn').instance()).toBeDisabled();
-      expect(wrapper.exists('a.pagination-btn')).toBeTruthy();
+      setupComponent('?page=1');
+      expect(screen.getByText('Previous')).toBeDisabled();
+      expect(screen.getByText('Next')).toBeInTheDocument();
     });
 
     it('renders disable next button and enabled prev link', () => {
-      const wrapper = mount(setupWrapper('?page=11'));
-      expect(wrapper.exists('a.pagination-btn')).toBeTruthy();
-      expect(wrapper.exists('button.pagination-btn')).toBeTruthy();
+      setupComponent('?page=11');
+      expect(screen.getByText('Previous')).toBeInTheDocument();
+      expect(screen.getByText('Next')).toBeDisabled();
     });
 
     it('renders next & prev links with correct path', () => {
-      const wrapper = mount(setupWrapper('?page=5&perPage=20'));
-      expect(wrapper.exists('a.pagination-btn')).toBeTruthy();
-      expect(wrapper.find('a.pagination-btn').at(0).prop('href')).toEqual(
+      setupComponent('?page=5&perPage=20');
+      expect(screen.getByText('Previous')).toBeInTheDocument();
+      expect(screen.getByText('Next')).toBeInTheDocument();
+      expect(screen.getByText('Previous')).toHaveAttribute(
+        'href',
         '/my/test/path/23?page=4&perPage=20'
       );
-      expect(wrapper.find('a.pagination-btn').at(1).prop('href')).toEqual(
+      expect(screen.getByText('Next')).toHaveAttribute(
+        'href',
         '/my/test/path/23?page=6&perPage=20'
       );
     });
@@ -43,49 +45,50 @@ describe('Pagination', () => {
 
   describe('spread', () => {
     it('renders 1 spread element after first page control', () => {
-      const wrapper = mount(setupWrapper('?page=8'));
-      expect(wrapper.find('span.pagination-ellipsis').length).toEqual(1);
-      expect(wrapper.find('ul li').at(1).text()).toEqual('…');
+      setupComponent('?page=8');
+      expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('…');
+      expect(screen.getAllByRole('listitem')[1].firstChild).toHaveClass(
+        'pagination-ellipsis'
+      );
     });
 
     it('renders 1 spread element before last spread control', () => {
-      const wrapper = mount(setupWrapper('?page=2'));
-      expect(wrapper.find('span.pagination-ellipsis').length).toEqual(1);
-      expect(wrapper.find('ul li').at(7).text()).toEqual('…');
+      setupComponent('?page=2');
+      expect(screen.getAllByRole('listitem')[7]).toHaveTextContent('…');
+      expect(screen.getAllByRole('listitem')[7].firstChild).toHaveClass(
+        'pagination-ellipsis'
+      );
     });
 
     it('renders 2 spread elements', () => {
-      const wrapper = mount(setupWrapper('?page=6'));
-      expect(wrapper.find('span.pagination-ellipsis').length).toEqual(2);
-      expect(wrapper.find('ul li').at(0).text()).toEqual('1');
-      expect(wrapper.find('ul li').at(1).text()).toEqual('…');
-      expect(wrapper.find('ul li').at(7).text()).toEqual('…');
-      expect(wrapper.find('ul li').at(8).text()).toEqual('11');
+      setupComponent('?page=6');
+      expect(screen.getAllByText('…').length).toEqual(2);
+      expect(screen.getAllByRole('listitem')[0]).toHaveTextContent('1');
+      expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('…');
+      expect(screen.getAllByRole('listitem')[7]).toHaveTextContent('…');
+      expect(screen.getAllByRole('listitem')[8]).toHaveTextContent('11');
     });
 
     it('renders 0 spread elements', () => {
-      const wrapper = mount(setupWrapper('?page=2', { totalPages: 8 }));
-      expect(wrapper.find('span.pagination-ellipsis').length).toEqual(0);
-      expect(wrapper.find('ul li').length).toEqual(8);
+      setupComponent('?page=2', { totalPages: 8 });
+      expect(screen.queryAllByText('…').length).toEqual(0);
+      expect(screen.getAllByRole('listitem').length).toEqual(8);
     });
   });
 
   describe('current page', () => {
-    it('adds is-current class to correct page if page param is set', () => {
-      const wrapper = mount(setupWrapper('?page=8'));
-      expect(wrapper.exists('a.pagination-link.is-current')).toBeTruthy();
-      expect(wrapper.find('a.pagination-link.is-current').text()).toEqual('8');
-    });
-
-    it('adds is-current class to correct page even if page param is not set', () => {
-      const wrapper = mount(setupWrapper('', { totalPages: 8 }));
-      expect(wrapper.exists('a.pagination-link.is-current')).toBeTruthy();
-      expect(wrapper.find('a.pagination-link.is-current').text()).toEqual('1');
+    it('check if it sets page 8 as current when page param is set', () => {
+      setupComponent('?page=8');
+      expect(screen.getByText('8')).toHaveStyle(
+        `background-color: ${theme.paginationStyles.currentPage}`
+      );
     });
 
-    it('adds no is-current class if page numder is invalid', () => {
-      const wrapper = mount(setupWrapper('?page=80'));
-      expect(wrapper.exists('a.pagination-link.is-current')).toBeFalsy();
+    it('check if it sets first page as current when page param not set', () => {
+      setupComponent('', { totalPages: 8 });
+      expect(screen.getByText('1')).toHaveStyle(
+        `background-color: ${theme.paginationStyles.currentPage}`
+      );
     });
   });
 });

+ 0 - 13
kafka-ui-react-app/src/components/common/Pagination/__tests__/__snapshots__/PageControl.spec.tsx.snap

@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PageControl matches snapshot 1`] = `
-<li>
-  <Link
-    aria-label="Goto page 138"
-    className="pagination-link is-current"
-    to="/test"
-  >
-    138
-  </Link>
-</li>
-`;

+ 1 - 0
kafka-ui-react-app/src/components/common/Select/Select.tsx

@@ -29,6 +29,7 @@ const Select: React.FC<SelectProps> = ({
       {isLive && <LiveIcon />}
       {name ? (
         <S.Select
+          role="listbox"
           selectSize={selectSize}
           isLive={isLive}
           {...methods.register(name, { ...hookFormOptions })}

+ 2 - 0
kafka-ui-react-app/src/components/common/Select/__tests__/__snapshots__/Select.spec.tsx.snap

@@ -82,6 +82,7 @@ exports[`Custom Select when live matches the snapshot 1`] = `
       </i>
       <select
         class="c2"
+        role="listbox"
       />
     </div>
   </div>
@@ -136,6 +137,7 @@ exports[`Custom Select when non-live matches the snapshot 1`] = `
     >
       <select
         class="c1"
+        role="listbox"
       />
     </div>
   </div>

+ 2 - 2
kafka-ui-react-app/src/lib/__test__/paths.spec.ts

@@ -60,8 +60,8 @@ describe('Paths', () => {
       `${paths.clusterSchemasPath(clusterName)}/${schemaId}`
     );
   });
-  it('clusterSchemaSchemaEditPath', () => {
-    expect(paths.clusterSchemaSchemaEditPath(clusterName, schemaId)).toEqual(
+  it('clusterSchemaEditPath', () => {
+    expect(paths.clusterSchemaEditPath(clusterName, schemaId)).toEqual(
       `${paths.clusterSchemaPath(clusterName, schemaId)}/edit`
     );
   });

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

@@ -39,7 +39,7 @@ export const clusterSchemaPath = (
   clusterName: ClusterName,
   subject: SchemaName
 ) => `${clusterSchemasPath(clusterName)}/${subject}`;
-export const clusterSchemaSchemaEditPath = (
+export const clusterSchemaEditPath = (
   clusterName: ClusterName,
   subject: SchemaName
 ) => `${clusterSchemasPath(clusterName)}/${subject}/edit`;

+ 33 - 12
kafka-ui-react-app/src/lib/testHelpers.tsx

@@ -3,10 +3,14 @@ import { MemoryRouter, Route, StaticRouter } from 'react-router-dom';
 import { Provider } from 'react-redux';
 import { mount } from 'enzyme';
 import { act } from 'react-dom/test-utils';
-import { store } from 'redux/store';
+import { store as appStore } from 'redux/store';
 import { ThemeProvider } from 'styled-components';
 import theme from 'theme/theme';
 import { render, 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';
 
 interface TestRouterWrapperProps {
   pathname: string;
@@ -45,7 +49,7 @@ export const containerRendersView = (
       let wrapper = mount(<div />);
       await act(async () => {
         wrapper = mount(
-          <Provider store={store}>
+          <Provider store={appStore}>
             <StaticRouter>
               <ThemeProvider theme={theme}>{container}</ThemeProvider>
             </StaticRouter>
@@ -65,18 +69,35 @@ export function mountWithTheme(child: ReactElement) {
   });
 }
 
-// overrides @testing-library/react render.
-const AllTheProviders: React.FC = ({ children }) => {
-  return (
-    <ThemeProvider theme={theme}>
-      <Provider store={store}>{children}</Provider>
-    </ThemeProvider>
-  );
-};
+interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
+  preloadedState?: Partial<RootState>;
+  store?: Store<Partial<RootState>, AnyAction>;
+  pathname?: string;
+}
 
 const customRender = (
   ui: ReactElement,
-  options?: Omit<RenderOptions, 'wrapper'>
-) => render(ui, { wrapper: AllTheProviders, ...options });
+  {
+    preloadedState,
+    store = configureStore<RootState>({
+      reducer: rootReducer,
+      preloadedState,
+    }),
+    pathname,
+    ...renderOptions
+  }: CustomRenderOptions = {}
+) => {
+  // overrides @testing-library/react render.
+  const AllTheProviders: React.FC = ({ children }) => {
+    return (
+      <ThemeProvider theme={theme}>
+        <Provider store={store}>
+          <StaticRouter location={{ pathname }}>{children}</StaticRouter>
+        </Provider>
+      </ThemeProvider>
+    );
+  };
+  return render(ui, { wrapper: AllTheProviders, ...renderOptions });
+};
 
 export { customRender as render };

+ 0 - 74
kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts

@@ -1,7 +1,3 @@
-import {
-  clusterSchemasPayload,
-  schemaVersionsPayload,
-} from 'redux/reducers/schemas/__test__/fixtures';
 import * as actions from 'redux/actions';
 import {
   MessageSchemaSourceEnum,
@@ -18,76 +14,6 @@ import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixture
 import { mockTopicsState } from './fixtures';
 
 describe('Actions', () => {
-  describe('fetchSchemasByClusterNameAction', () => {
-    it('creates a REQUEST action', () => {
-      expect(actions.fetchSchemasByClusterNameAction.request()).toEqual({
-        type: 'GET_CLUSTER_SCHEMAS__REQUEST',
-      });
-    });
-
-    it('creates a SUCCESS action', () => {
-      expect(
-        actions.fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
-      ).toEqual({
-        type: 'GET_CLUSTER_SCHEMAS__SUCCESS',
-        payload: clusterSchemasPayload,
-      });
-    });
-
-    it('creates a FAILURE action', () => {
-      expect(actions.fetchSchemasByClusterNameAction.failure()).toEqual({
-        type: 'GET_CLUSTER_SCHEMAS__FAILURE',
-      });
-    });
-  });
-
-  describe('fetchSchemaVersionsAction', () => {
-    it('creates a REQUEST action', () => {
-      expect(actions.fetchSchemaVersionsAction.request()).toEqual({
-        type: 'GET_SCHEMA_VERSIONS__REQUEST',
-      });
-    });
-
-    it('creates a SUCCESS action', () => {
-      expect(
-        actions.fetchSchemaVersionsAction.success(schemaVersionsPayload)
-      ).toEqual({
-        type: 'GET_SCHEMA_VERSIONS__SUCCESS',
-        payload: schemaVersionsPayload,
-      });
-    });
-
-    it('creates a FAILURE action', () => {
-      expect(actions.fetchSchemaVersionsAction.failure()).toEqual({
-        type: 'GET_SCHEMA_VERSIONS__FAILURE',
-      });
-    });
-  });
-
-  describe('createSchemaAction', () => {
-    it('creates a REQUEST action', () => {
-      expect(actions.createSchemaAction.request()).toEqual({
-        type: 'POST_SCHEMA__REQUEST',
-      });
-    });
-
-    it('creates a SUCCESS action', () => {
-      expect(
-        actions.createSchemaAction.success(schemaVersionsPayload[0])
-      ).toEqual({
-        type: 'POST_SCHEMA__SUCCESS',
-        payload: schemaVersionsPayload[0],
-      });
-    });
-
-    it('creates a FAILURE action', () => {
-      expect(actions.createSchemaAction.failure({})).toEqual({
-        type: 'POST_SCHEMA__FAILURE',
-        payload: {},
-      });
-    });
-  });
-
   describe('dismissAlert', () => {
     it('creates a REQUEST action', () => {
       const id = 'alert-id1';

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

@@ -1,268 +0,0 @@
-import fetchMock from 'fetch-mock-jest';
-import * as actions from 'redux/actions/actions';
-import * as thunks from 'redux/actions/thunks';
-import * as schemaFixtures from 'redux/reducers/schemas/__test__/fixtures';
-import {
-  CompatibilityLevelCompatibilityEnum,
-  SchemaType,
-} from 'generated-sources';
-import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
-import * as fixtures from 'redux/actions/__test__/fixtures';
-
-const store = mockStoreCreator;
-
-const clusterName = 'local';
-const subject = 'test';
-
-describe('Thunks', () => {
-  afterEach(() => {
-    fetchMock.restore();
-    store.clearActions();
-  });
-
-  describe('fetchSchemasByClusterName', () => {
-    it('creates GET_CLUSTER_SCHEMAS__SUCCESS when fetching cluster schemas', async () => {
-      fetchMock.getOnce(`/api/clusters/${clusterName}/schemas`, {
-        body: schemaFixtures.clusterSchemasPayload,
-      });
-      await store.dispatch(thunks.fetchSchemasByClusterName(clusterName));
-      expect(store.getActions()).toEqual([
-        actions.fetchSchemasByClusterNameAction.request(),
-        actions.fetchSchemasByClusterNameAction.success(
-          schemaFixtures.clusterSchemasPayload
-        ),
-      ]);
-    });
-
-    it('creates GET_CLUSTER_SCHEMAS__FAILURE when fetching cluster schemas', async () => {
-      fetchMock.getOnce(`/api/clusters/${clusterName}/schemas`, 404);
-      await store.dispatch(thunks.fetchSchemasByClusterName(clusterName));
-      expect(store.getActions()).toEqual([
-        actions.fetchSchemasByClusterNameAction.request(),
-        actions.fetchSchemasByClusterNameAction.failure(),
-      ]);
-    });
-  });
-
-  describe('fetchSchemaVersions', () => {
-    it('creates GET_SCHEMA_VERSIONS__SUCCESS when fetching schema versions', async () => {
-      fetchMock.getOnce(
-        `/api/clusters/${clusterName}/schemas/${subject}/versions`,
-        {
-          body: schemaFixtures.schemaVersionsPayload,
-        }
-      );
-      await store.dispatch(thunks.fetchSchemaVersions(clusterName, subject));
-      expect(store.getActions()).toEqual([
-        actions.fetchSchemaVersionsAction.request(),
-        actions.fetchSchemaVersionsAction.success(
-          schemaFixtures.schemaVersionsPayload
-        ),
-      ]);
-    });
-
-    it('creates GET_SCHEMA_VERSIONS__FAILURE when fetching schema versions', async () => {
-      fetchMock.getOnce(
-        `/api/clusters/${clusterName}/schemas/${subject}/versions`,
-        404
-      );
-      await store.dispatch(thunks.fetchSchemaVersions(clusterName, subject));
-      expect(store.getActions()).toEqual([
-        actions.fetchSchemaVersionsAction.request(),
-        actions.fetchSchemaVersionsAction.failure(),
-      ]);
-    });
-  });
-
-  describe('createSchema', () => {
-    it('creates POST_SCHEMA__SUCCESS when posting new schema', async () => {
-      fetchMock.postOnce(`/api/clusters/${clusterName}/schemas`, {
-        body: schemaFixtures.schemaVersionsPayload[0],
-      });
-      await store.dispatch(
-        thunks.createSchema(clusterName, fixtures.schemaPayload)
-      );
-      expect(store.getActions()).toEqual([
-        actions.createSchemaAction.request(),
-        actions.createSchemaAction.success(
-          schemaFixtures.schemaVersionsPayload[0]
-        ),
-      ]);
-    });
-
-    it('creates POST_SCHEMA__FAILURE when posting new schema', async () => {
-      fetchMock.postOnce(`/api/clusters/${clusterName}/schemas`, 404);
-      try {
-        await store.dispatch(
-          thunks.createSchema(clusterName, fixtures.schemaPayload)
-        );
-      } catch (error) {
-        expect(store.getActions()).toEqual([
-          actions.createSchemaAction.request(),
-          actions.createSchemaAction.failure({
-            alert: {
-              response: {
-                status: 404,
-                statusText: 'Not Found',
-                url: `/api/clusters/${clusterName}/schemas`,
-              },
-              subject: 'schema-NewSchema',
-              title: 'Schema NewSchema',
-            },
-          }),
-        ]);
-      }
-    });
-  });
-
-  describe('deleteSchema', () => {
-    it('fires DELETE_SCHEMA__SUCCESS on success', async () => {
-      fetchMock.deleteOnce(
-        `/api/clusters/${clusterName}/schemas/${subject}`,
-        200
-      );
-
-      await store.dispatch(thunks.deleteSchema(clusterName, subject));
-
-      expect(store.getActions()).toEqual([
-        actions.deleteSchemaAction.request(),
-        actions.deleteSchemaAction.success(subject),
-      ]);
-    });
-
-    it('fires DELETE_SCHEMA__FAILURE on failure', async () => {
-      fetchMock.deleteOnce(
-        `/api/clusters/${clusterName}/schemas/${subject}`,
-        404
-      );
-
-      try {
-        await store.dispatch(thunks.deleteSchema(clusterName, subject));
-      } catch (error) {
-        expect(error.status).toEqual(404);
-        expect(store.getActions()).toEqual([
-          actions.deleteSchemaAction.request(),
-          actions.deleteSchemaAction.failure({}),
-        ]);
-      }
-    });
-  });
-
-  describe('updateSchema', () => {
-    it('calls PATCH_SCHEMA__REQUEST', () => {
-      store.dispatch(
-        thunks.updateSchema(
-          fixtures.schema,
-          fixtures.schemaPayload.schema,
-          SchemaType.AVRO,
-          CompatibilityLevelCompatibilityEnum.BACKWARD,
-          clusterName,
-          subject
-        )
-      );
-      expect(store.getActions()).toEqual([
-        actions.updateSchemaAction.request(),
-      ]);
-    });
-  });
-
-  describe('fetchGlobalSchemaCompatibilityLevel', () => {
-    it('calls GET_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST on the fucntion call', () => {
-      store.dispatch(thunks.fetchGlobalSchemaCompatibilityLevel(clusterName));
-      expect(store.getActions()).toEqual([
-        actions.fetchGlobalSchemaCompatibilityLevelAction.request(),
-      ]);
-    });
-
-    it('calls GET_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS on a successful API call', async () => {
-      fetchMock.getOnce(`/api/clusters/${clusterName}/schemas/compatibility`, {
-        compatibility: CompatibilityLevelCompatibilityEnum.FORWARD,
-      });
-      await store.dispatch(
-        thunks.fetchGlobalSchemaCompatibilityLevel(clusterName)
-      );
-      expect(store.getActions()).toEqual([
-        actions.fetchGlobalSchemaCompatibilityLevelAction.request(),
-        actions.fetchGlobalSchemaCompatibilityLevelAction.success(
-          CompatibilityLevelCompatibilityEnum.FORWARD
-        ),
-      ]);
-    });
-
-    it('calls GET_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE on an unsuccessful API call', async () => {
-      fetchMock.getOnce(
-        `/api/clusters/${clusterName}/schemas/compatibility`,
-        404
-      );
-      try {
-        await store.dispatch(
-          thunks.fetchGlobalSchemaCompatibilityLevel(clusterName)
-        );
-      } catch (error) {
-        expect(error.status).toEqual(404);
-        expect(store.getActions()).toEqual([
-          actions.fetchGlobalSchemaCompatibilityLevelAction.request(),
-          actions.fetchGlobalSchemaCompatibilityLevelAction.failure(),
-        ]);
-      }
-    });
-  });
-
-  describe('updateGlobalSchemaCompatibilityLevel', () => {
-    const compatibilityLevel = CompatibilityLevelCompatibilityEnum.FORWARD;
-    it('calls POST_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST on the fucntion call', () => {
-      store.dispatch(
-        thunks.updateGlobalSchemaCompatibilityLevel(
-          clusterName,
-          compatibilityLevel
-        )
-      );
-      expect(store.getActions()).toEqual([
-        actions.updateGlobalSchemaCompatibilityLevelAction.request(),
-      ]);
-    });
-
-    it('calls POST_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS on a successful API call', async () => {
-      fetchMock.putOnce(
-        `/api/clusters/${clusterName}/schemas/compatibility`,
-        200
-      );
-      fetchMock.getOnce(`/api/clusters/${clusterName}/schemas`, 200);
-
-      await store.dispatch(
-        thunks.updateGlobalSchemaCompatibilityLevel(
-          clusterName,
-          compatibilityLevel
-        )
-      );
-      expect(store.getActions()).toEqual([
-        actions.updateGlobalSchemaCompatibilityLevelAction.request(),
-        actions.fetchSchemasByClusterNameAction.request(),
-        actions.updateGlobalSchemaCompatibilityLevelAction.success(
-          CompatibilityLevelCompatibilityEnum.FORWARD
-        ),
-      ]);
-    });
-
-    it('calls POST_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE on an unsuccessful API call', async () => {
-      fetchMock.putOnce(
-        `/api/clusters/${clusterName}/schemas/compatibility`,
-        404
-      );
-      try {
-        await store.dispatch(
-          thunks.updateGlobalSchemaCompatibilityLevel(
-            clusterName,
-            compatibilityLevel
-          )
-        );
-      } catch (error) {
-        expect(error.status).toEqual(404);
-        expect(store.getActions()).toEqual([
-          actions.updateGlobalSchemaCompatibilityLevelAction.request(),
-          actions.updateGlobalSchemaCompatibilityLevelAction.failure(),
-        ]);
-      }
-    });
-  });
-});

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

@@ -7,8 +7,6 @@ import {
   ConnectorConfig,
 } from 'redux/interfaces';
 import {
-  SchemaSubject,
-  CompatibilityLevelCompatibilityEnum,
   TopicColumnsToSort,
   Connector,
   FullConnectorInfo,
@@ -63,48 +61,6 @@ export const deleteTopicAction = createAsyncAction(
   'DELETE_TOPIC__CANCEL'
 )<undefined, TopicName, undefined, undefined>();
 
-export const fetchSchemasByClusterNameAction = createAsyncAction(
-  'GET_CLUSTER_SCHEMAS__REQUEST',
-  'GET_CLUSTER_SCHEMAS__SUCCESS',
-  'GET_CLUSTER_SCHEMAS__FAILURE'
-)<undefined, SchemaSubject[], undefined>();
-
-export const fetchGlobalSchemaCompatibilityLevelAction = createAsyncAction(
-  'GET_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST',
-  'GET_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS',
-  'GET_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE'
-)<undefined, CompatibilityLevelCompatibilityEnum, undefined>();
-
-export const updateGlobalSchemaCompatibilityLevelAction = createAsyncAction(
-  'PUT_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST',
-  'PUT_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS',
-  'PUT_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE'
-)<undefined, CompatibilityLevelCompatibilityEnum, undefined>();
-
-export const fetchSchemaVersionsAction = createAsyncAction(
-  'GET_SCHEMA_VERSIONS__REQUEST',
-  'GET_SCHEMA_VERSIONS__SUCCESS',
-  'GET_SCHEMA_VERSIONS__FAILURE'
-)<undefined, SchemaSubject[], undefined>();
-
-export const createSchemaAction = createAsyncAction(
-  'POST_SCHEMA__REQUEST',
-  'POST_SCHEMA__SUCCESS',
-  'POST_SCHEMA__FAILURE'
-)<undefined, SchemaSubject, { alert?: FailurePayload }>();
-
-export const updateSchemaAction = createAsyncAction(
-  'PATCH_SCHEMA__REQUEST',
-  'PATCH_SCHEMA__SUCCESS',
-  'PATCH_SCHEMA__FAILURE'
-)<undefined, SchemaSubject, { alert?: FailurePayload }>();
-
-export const deleteSchemaAction = createAsyncAction(
-  'DELETE_SCHEMA__REQUEST',
-  'DELETE_SCHEMA__SUCCESS',
-  'DELETE_SCHEMA__FAILURE'
-)<undefined, string, { alert?: FailurePayload }>();
-
 export const dismissAlert = createAction('DISMISS_ALERT')<string>();
 
 export const fetchConnectsAction = createAsyncAction(

+ 0 - 1
kafka-ui-react-app/src/redux/actions/thunks/index.ts

@@ -1,4 +1,3 @@
-export * from './schemas';
 export * from './topics';
 export * from './connectors';
 export * from './ksqlDb';

+ 0 - 184
kafka-ui-react-app/src/redux/actions/thunks/schemas.ts

@@ -1,184 +0,0 @@
-import {
-  SchemasApi,
-  Configuration,
-  NewSchemaSubject,
-  SchemaSubject,
-  CompatibilityLevelCompatibilityEnum,
-  SchemaType,
-} from 'generated-sources';
-import {
-  PromiseThunkResult,
-  ClusterName,
-  SchemaName,
-  FailurePayload,
-} from 'redux/interfaces';
-import { BASE_PARAMS } from 'lib/constants';
-import * as actions from 'redux/actions';
-import { getResponse } from 'lib/errorHandling';
-import { isEqual } from 'lodash';
-
-const apiClientConf = new Configuration(BASE_PARAMS);
-export const schemasApiClient = new SchemasApi(apiClientConf);
-
-export const fetchSchemasByClusterName =
-  (clusterName: ClusterName): PromiseThunkResult<void> =>
-  async (dispatch) => {
-    dispatch(actions.fetchSchemasByClusterNameAction.request());
-    try {
-      const schemas = await schemasApiClient.getSchemas({ clusterName });
-      dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
-    } catch (e) {
-      dispatch(actions.fetchSchemasByClusterNameAction.failure());
-    }
-  };
-
-export const fetchSchemaVersions =
-  (clusterName: ClusterName, subject: SchemaName): PromiseThunkResult<void> =>
-  async (dispatch) => {
-    if (!subject) return;
-    dispatch(actions.fetchSchemaVersionsAction.request());
-    try {
-      const versions = await schemasApiClient.getAllVersionsBySubject({
-        clusterName,
-        subject,
-      });
-      dispatch(actions.fetchSchemaVersionsAction.success(versions));
-    } catch (e) {
-      dispatch(actions.fetchSchemaVersionsAction.failure());
-    }
-  };
-
-export const fetchGlobalSchemaCompatibilityLevel =
-  (clusterName: ClusterName): PromiseThunkResult<void> =>
-  async (dispatch) => {
-    dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.request());
-    try {
-      const result = await schemasApiClient.getGlobalSchemaCompatibilityLevel({
-        clusterName,
-      });
-      dispatch(
-        actions.fetchGlobalSchemaCompatibilityLevelAction.success(
-          result.compatibility
-        )
-      );
-    } catch (e) {
-      dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.failure());
-    }
-  };
-
-export const updateGlobalSchemaCompatibilityLevel =
-  (
-    clusterName: ClusterName,
-    compatibilityLevel: CompatibilityLevelCompatibilityEnum
-  ): PromiseThunkResult<void> =>
-  async (dispatch) => {
-    dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.request());
-    try {
-      await schemasApiClient.updateGlobalSchemaCompatibilityLevel({
-        clusterName,
-        compatibilityLevel: { compatibility: compatibilityLevel },
-      });
-      dispatch(fetchSchemasByClusterName(clusterName));
-      dispatch(
-        actions.updateGlobalSchemaCompatibilityLevelAction.success(
-          compatibilityLevel
-        )
-      );
-    } catch (e) {
-      dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.failure());
-    }
-  };
-
-export const createSchema =
-  (
-    clusterName: ClusterName,
-    newSchemaSubject: NewSchemaSubject
-  ): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.createSchemaAction.request());
-    try {
-      const schema: SchemaSubject = await schemasApiClient.createNewSchema({
-        clusterName,
-        newSchemaSubject,
-      });
-      dispatch(actions.createSchemaAction.success(schema));
-    } catch (error) {
-      const response = await getResponse(error);
-      const alert: FailurePayload = {
-        subject: ['schema', newSchemaSubject.subject].join('-'),
-        title: `Schema ${newSchemaSubject.subject}`,
-        response,
-      };
-      dispatch(actions.createSchemaAction.failure({ alert }));
-      throw error;
-    }
-  };
-
-export const updateSchema =
-  (
-    latestSchema: SchemaSubject,
-    newSchema: string,
-    newSchemaType: SchemaType,
-    newCompatibilityLevel: CompatibilityLevelCompatibilityEnum,
-    clusterName: string,
-    subject: string
-  ): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.updateSchemaAction.request());
-    try {
-      let schema: SchemaSubject = latestSchema;
-      if (
-        (newSchema &&
-          !isEqual(JSON.parse(latestSchema.schema), JSON.parse(newSchema))) ||
-        newSchemaType !== latestSchema.schemaType
-      ) {
-        schema = await schemasApiClient.createNewSchema({
-          clusterName,
-          newSchemaSubject: {
-            ...latestSchema,
-            schema: newSchema || latestSchema.schema,
-            schemaType: newSchemaType || latestSchema.schemaType,
-          },
-        });
-      }
-      if (newCompatibilityLevel !== latestSchema.compatibilityLevel) {
-        await schemasApiClient.updateSchemaCompatibilityLevel({
-          clusterName,
-          subject,
-          compatibilityLevel: {
-            compatibility: newCompatibilityLevel,
-          },
-        });
-      }
-      actions.updateSchemaAction.success(schema);
-    } catch (e) {
-      const response = await getResponse(e);
-      const alert: FailurePayload = {
-        subject: ['schema', subject].join('-'),
-        title: `Schema ${subject}`,
-        response,
-      };
-      dispatch(actions.updateSchemaAction.failure({ alert }));
-      throw e;
-    }
-  };
-export const deleteSchema =
-  (clusterName: ClusterName, subject: string): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.deleteSchemaAction.request());
-    try {
-      await schemasApiClient.deleteSchema({
-        clusterName,
-        subject,
-      });
-      dispatch(actions.deleteSchemaAction.success(subject));
-    } catch (error) {
-      const response = await getResponse(error);
-      const alert: FailurePayload = {
-        subject: ['schema', subject].join('-'),
-        title: `Schema ${subject}`,
-        response,
-      };
-      dispatch(actions.deleteSchemaAction.failure({ alert }));
-    }
-  };

+ 24 - 11
kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts

@@ -1,6 +1,7 @@
 import {
   createEntityAdapter,
   createSlice,
+  nanoid,
   PayloadAction,
 } from '@reduxjs/toolkit';
 import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers';
@@ -18,6 +19,20 @@ const isServerResponse = (payload: unknown): payload is ServerResponse => {
   return false;
 };
 
+const transformResponseToAlert = (payload: ServerResponse) => {
+  const { status, statusText, message, url } = payload;
+  const alert: Alert = {
+    id: url || nanoid(),
+    type: 'error',
+    title: `${status} ${statusText}`,
+    message,
+    response: payload,
+    createdAt: now(),
+  };
+
+  return alert;
+};
+
 const alertsSlice = createSlice({
   name: 'alerts',
   initialState: alertsAdapter.getInitialState(),
@@ -26,6 +41,12 @@ const alertsSlice = createSlice({
     alertAdded(state, action: PayloadAction<Alert>) {
       alertsAdapter.upsertOne(state, action.payload);
     },
+    serverErrorAlertAdded: (
+      state,
+      { payload }: PayloadAction<ServerResponse>
+    ) => {
+      alertsAdapter.upsertOne(state, transformResponseToAlert(payload));
+    },
   },
   extraReducers: (builder) => {
     builder.addMatcher(
@@ -34,16 +55,7 @@ const alertsSlice = createSlice({
       (state, { meta, payload }) => {
         const { rejectedWithValue } = meta;
         if (rejectedWithValue && isServerResponse(payload)) {
-          const { status, statusText, message, url } = payload;
-          const alert: Alert = {
-            id: url || meta.requestId,
-            type: 'error',
-            title: `${status} ${statusText}`,
-            message,
-            response: payload,
-            createdAt: now(),
-          };
-          alertsAdapter.addOne(state, alert);
+          alertsAdapter.upsertOne(state, transformResponseToAlert(payload));
         }
       }
     );
@@ -54,6 +66,7 @@ export const { selectAll } = alertsAdapter.getSelectors<RootState>(
   (state) => state.alerts
 );
 
-export const { alertDissmissed, alertAdded } = alertsSlice.actions;
+export const { alertDissmissed, alertAdded, serverErrorAlertAdded } =
+  alertsSlice.actions;
 
 export default alertsSlice.reducer;

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

@@ -3,11 +3,11 @@ import clusters from 'redux/reducers/clusters/clustersSlice';
 import loader from 'redux/reducers/loader/loaderSlice';
 import brokers from 'redux/reducers/brokers/brokersSlice';
 import alerts from 'redux/reducers/alerts/alertsSlice';
+import schemas from 'redux/reducers/schemas/schemasSlice';
 
 import topics from './topics/reducer';
 import topicMessages from './topicMessages/reducer';
 import consumerGroups from './consumerGroups/consumerGroupsSlice';
-import schemas from './schemas/reducer';
 import connect from './connect/reducer';
 import ksqlDb from './ksqlDb/reducer';
 import legacyLoader from './loader/reducer';

+ 0 - 82
kafka-ui-react-app/src/redux/reducers/schemas/__test__/__snapshots__/reducer.spec.ts.snap

@@ -1,82 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Schemas reducer reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload 1`] = `
-Object {
-  "allNames": Array [
-    "test2",
-    "test3",
-    "test",
-  ],
-  "byName": Object {
-    "test": Object {
-      "compatibilityLevel": "BACKWARD",
-      "id": 2,
-      "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-      "schemaType": "JSON",
-      "subject": "test",
-      "version": "2",
-    },
-    "test2": Object {
-      "compatibilityLevel": "BACKWARD",
-      "id": 4,
-      "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord4\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-      "schemaType": "JSON",
-      "subject": "test2",
-      "version": "3",
-    },
-    "test3": Object {
-      "compatibilityLevel": "BACKWARD",
-      "id": 5,
-      "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-      "schemaType": "JSON",
-      "subject": "test3",
-      "version": "1",
-    },
-  },
-  "currentSchemaVersions": Array [],
-}
-`;
-
-exports[`Schemas reducer reacts on GET_SCHEMA_VERSIONS__SUCCESS and returns payload 1`] = `
-Object {
-  "allNames": Array [],
-  "byName": Object {},
-  "currentSchemaVersions": Array [
-    Object {
-      "compatibilityLevel": "BACKWARD",
-      "id": 1,
-      "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-      "schemaType": "JSON",
-      "subject": "test",
-      "version": "1",
-    },
-    Object {
-      "compatibilityLevel": "BACKWARD",
-      "id": 2,
-      "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-      "schemaType": "JSON",
-      "subject": "test",
-      "version": "2",
-    },
-  ],
-}
-`;
-
-exports[`Schemas reducer reacts on POST_SCHEMA__SUCCESS and returns payload 1`] = `
-Object {
-  "allNames": Array [
-    "test",
-  ],
-  "byName": Object {
-    "test": Object {
-      "compatibilityLevel": "BACKWARD",
-      "id": 1,
-      "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-      "schemaType": "JSON",
-      "subject": "test",
-      "version": "1",
-    },
-  },
-  "currentSchemaVersions": Array [],
-}
-`;

+ 31 - 100
kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts

@@ -1,108 +1,39 @@
-import { SchemasState } from 'redux/interfaces';
-import { SchemaSubject, SchemaType } from 'generated-sources';
+import { SchemaType } from 'generated-sources';
 
-export const initialState: SchemasState = {
-  byName: {},
-  allNames: [],
-  currentSchemaVersions: [],
-};
-
-export const clusterSchemasPayload: SchemaSubject[] = [
-  {
-    subject: 'test2',
-    version: '3',
-    id: 4,
-    schema:
-      '{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
-  },
-  {
-    subject: 'test3',
-    version: '1',
-    id: 5,
-    schema:
-      '{"type":"record","name":"MyRecord","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
-  },
-  {
-    subject: 'test',
-    version: '2',
-    id: 2,
-    schema:
-      '{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
-  },
-];
-
-export const schemaVersionsPayload: SchemaSubject[] = [
-  {
-    subject: 'test',
-    version: '1',
-    id: 1,
-    schema:
-      '{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
-  },
-  {
-    subject: 'test',
-    version: '2',
-    id: 2,
-    schema:
-      '{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
-  },
-];
-
-export const newSchemaPayload: SchemaSubject = {
-  subject: 'test4',
-  version: '2',
+export const schemaVersion = {
+  subject: 'schema7_1',
+  version: '1',
   id: 2,
   schema:
-    '{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-  compatibilityLevel: 'BACKWARD',
+    '{"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://example.com/myURI.schema.json","title":"TestRecord","type":"object","additionalProperties":false,"properties":{"f1":{"type":"integer"},"f2":{"type":"string"},"schema":{"type":"string"}}}',
+  compatibilityLevel: 'FULL',
   schemaType: SchemaType.JSON,
 };
 
-export const clusterSchemasPayloadWithNewSchema: SchemaSubject[] = [
-  {
-    subject: 'test2',
-    version: '3',
-    id: 4,
-    schema:
-      '{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
-  },
-  {
-    subject: 'test3',
-    version: '1',
-    id: 5,
-    schema:
-      '{"type":"record","name":"MyRecord","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
-  },
-  {
-    subject: 'test',
-    version: '2',
-    id: 2,
-    schema:
-      '{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
+export const schemasFulfilledState = {
+  ids: ['MySchemaSubject', 'schema7_1'],
+  entities: {
+    MySchemaSubject: {
+      subject: 'MySchemaSubject',
+      version: '1',
+      id: 28,
+      schema: '12',
+      compatibilityLevel: 'FORWARD_TRANSITIVE',
+      schemaType: SchemaType.JSON,
+    },
+    schema7_1: schemaVersion,
+  },
+  versions: {
+    ids: [],
+    entities: {},
   },
-  {
-    subject: 'test4',
-    version: '2',
-    id: 2,
-    schema:
-      '{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
-    compatibilityLevel: 'BACKWARD',
-    schemaType: SchemaType.JSON,
+};
+
+export const schemasInitialState = {
+  ids: [],
+  entities: {},
+  versions: {
+    ids: [],
+    entities: {},
   },
-];
+};

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

@@ -1,118 +0,0 @@
-import {
-  CompatibilityLevelCompatibilityEnum,
-  SchemaSubject,
-  SchemaType,
-} from 'generated-sources';
-import {
-  createSchemaAction,
-  deleteSchemaAction,
-  fetchGlobalSchemaCompatibilityLevelAction,
-  fetchSchemasByClusterNameAction,
-  fetchSchemaVersionsAction,
-} from 'redux/actions';
-import reducer from 'redux/reducers/schemas/reducer';
-
-import {
-  clusterSchemasPayload,
-  initialState,
-  schemaVersionsPayload,
-} from './fixtures';
-
-describe('Schemas reducer', () => {
-  it('returns the initial state', () => {
-    expect(
-      reducer(undefined, fetchSchemasByClusterNameAction.request())
-    ).toEqual(initialState);
-    expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
-      initialState
-    );
-    expect(reducer(undefined, createSchemaAction.request())).toEqual(
-      initialState
-    );
-  });
-
-  it('reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload', () => {
-    expect(
-      reducer(
-        undefined,
-        fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
-      )
-    ).toMatchSnapshot();
-  });
-
-  it('reacts on GET_SCHEMA_VERSIONS__SUCCESS and returns payload', () => {
-    expect(
-      reducer(
-        undefined,
-        fetchSchemaVersionsAction.success(schemaVersionsPayload)
-      )
-    ).toMatchSnapshot();
-  });
-
-  it('reacts on POST_SCHEMA__SUCCESS and returns payload', () => {
-    expect(
-      reducer(undefined, createSchemaAction.success(schemaVersionsPayload[0]))
-    ).toMatchSnapshot();
-  });
-
-  it('deletes the schema from the list on DELETE_SCHEMA__SUCCESS', () => {
-    const schema: SchemaSubject = {
-      subject: 'name',
-      version: '1',
-      id: 1,
-      schema: '{}',
-      compatibilityLevel: 'BACKWARD',
-      schemaType: SchemaType.AVRO,
-    };
-    expect(
-      reducer(
-        {
-          byName: {
-            [schema.subject]: schema,
-          },
-          allNames: [schema.subject],
-          currentSchemaVersions: [],
-        },
-        deleteSchemaAction.success(schema.subject)
-      )
-    ).toEqual({
-      byName: {},
-      allNames: [],
-      currentSchemaVersions: [],
-    });
-  });
-
-  it('adds global compatibility on successful fetch', () => {
-    expect(
-      reducer(
-        initialState,
-        fetchGlobalSchemaCompatibilityLevelAction.success(
-          CompatibilityLevelCompatibilityEnum.BACKWARD
-        )
-      )
-    ).toEqual({
-      ...initialState,
-      globalSchemaCompatibilityLevel:
-        CompatibilityLevelCompatibilityEnum.BACKWARD,
-    });
-  });
-
-  it('replaces global compatibility on successful update', () => {
-    expect(
-      reducer(
-        {
-          ...initialState,
-          globalSchemaCompatibilityLevel:
-            CompatibilityLevelCompatibilityEnum.FORWARD,
-        },
-        fetchGlobalSchemaCompatibilityLevelAction.success(
-          CompatibilityLevelCompatibilityEnum.BACKWARD
-        )
-      )
-    ).toEqual({
-      ...initialState,
-      globalSchemaCompatibilityLevel:
-        CompatibilityLevelCompatibilityEnum.BACKWARD,
-    });
-  });
-});

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

@@ -1,89 +0,0 @@
-import { orderBy } from 'lodash';
-import {
-  createSchemaAction,
-  fetchGlobalSchemaCompatibilityLevelAction,
-  fetchSchemasByClusterNameAction,
-  fetchSchemaVersionsAction,
-} from 'redux/actions';
-import { store } from 'redux/store';
-import * as selectors from 'redux/reducers/schemas/selectors';
-import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
-
-import {
-  clusterSchemasPayload,
-  clusterSchemasPayloadWithNewSchema,
-  newSchemaPayload,
-  schemaVersionsPayload,
-} from './fixtures';
-
-describe('Schemas selectors', () => {
-  describe('Initial state', () => {
-    it('returns fetch status', () => {
-      expect(selectors.getIsSchemaListFetched(store.getState())).toBeFalsy();
-      expect(selectors.getIsSchemaVersionFetched(store.getState())).toBeFalsy();
-      expect(selectors.getSchemaCreated(store.getState())).toBeFalsy();
-    });
-
-    it('returns schema list', () => {
-      expect(selectors.getSchemaList(store.getState())).toEqual([]);
-    });
-
-    it('returns undefined schema', () => {
-      expect(selectors.getSchema(store.getState(), ' ')).toBeUndefined();
-    });
-
-    it('returns sorted versions of schema', () => {
-      expect(selectors.getSortedSchemaVersions(store.getState())).toEqual([]);
-    });
-  });
-
-  describe('state', () => {
-    beforeAll(() => {
-      store.dispatch(
-        fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
-      );
-      store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload));
-      store.dispatch(createSchemaAction.success(newSchemaPayload));
-      store.dispatch(
-        fetchGlobalSchemaCompatibilityLevelAction.success(
-          CompatibilityLevelCompatibilityEnum.BACKWARD
-        )
-      );
-    });
-
-    it('returns fetch status', () => {
-      expect(selectors.getIsSchemaListFetched(store.getState())).toBeTruthy();
-      expect(
-        selectors.getIsSchemaVersionFetched(store.getState())
-      ).toBeTruthy();
-      expect(selectors.getSchemaCreated(store.getState())).toBeTruthy();
-      expect(
-        selectors.getGlobalSchemaCompatibilityLevelFetched(store.getState())
-      ).toBeTruthy();
-    });
-
-    it('returns schema list', () => {
-      expect(selectors.getSchemaList(store.getState())).toEqual(
-        clusterSchemasPayloadWithNewSchema
-      );
-    });
-
-    it('returns schema', () => {
-      expect(selectors.getSchema(store.getState(), 'test2')).toEqual(
-        clusterSchemasPayload[0]
-      );
-    });
-
-    it('returns ordered versions of schema', () => {
-      expect(selectors.getSortedSchemaVersions(store.getState())).toEqual(
-        orderBy(schemaVersionsPayload, 'id', 'desc')
-      );
-    });
-
-    it('return registry compatibility level', () => {
-      expect(
-        selectors.getGlobalSchemaCompatibilityLevel(store.getState())
-      ).toEqual(CompatibilityLevelCompatibilityEnum.BACKWARD);
-    });
-  });
-});

+ 0 - 76
kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts

@@ -1,76 +0,0 @@
-import { SchemaSubject } from 'generated-sources';
-import { Action, SchemasState } from 'redux/interfaces';
-import * as actions from 'redux/actions';
-import { getType } from 'typesafe-actions';
-
-export const initialState: SchemasState = {
-  byName: {},
-  allNames: [],
-  currentSchemaVersions: [],
-};
-
-const updateSchemaList = (
-  state: SchemasState,
-  payload: SchemaSubject[]
-): SchemasState => {
-  const initialMemo: SchemasState = {
-    ...state,
-    allNames: [],
-  };
-
-  return payload.reduce((memo: SchemasState, schema) => {
-    if (!schema.subject) return memo;
-    return {
-      ...memo,
-      byName: {
-        ...memo.byName,
-        [schema.subject]: {
-          ...memo.byName[schema.subject],
-          ...schema,
-        },
-      },
-      allNames: [...memo.allNames, schema.subject],
-    };
-  }, initialMemo);
-};
-
-const deleteFromSchemaList = (
-  state: SchemasState,
-  payload: string
-): SchemasState => {
-  const newState: SchemasState = {
-    ...state,
-  };
-  delete newState.byName[payload];
-  newState.allNames = newState.allNames.filter((name) => name !== payload);
-  return newState;
-};
-
-const reducer = (state = initialState, action: Action): SchemasState => {
-  switch (action.type) {
-    case 'GET_CLUSTER_SCHEMAS__SUCCESS':
-      return updateSchemaList(state, action.payload);
-    case 'GET_SCHEMA_VERSIONS__SUCCESS':
-      return { ...state, currentSchemaVersions: action.payload };
-    case 'POST_SCHEMA__SUCCESS':
-    case 'PATCH_SCHEMA__SUCCESS':
-      return {
-        ...state,
-        allNames: [...state.allNames, action.payload.subject],
-        byName: {
-          ...state.byName,
-          [action.payload.subject]: { ...action.payload },
-        },
-      };
-    case getType(actions.deleteSchemaAction.success):
-      return deleteFromSchemaList(state, action.payload);
-    case getType(actions.fetchGlobalSchemaCompatibilityLevelAction.success):
-      return { ...state, globalSchemaCompatibilityLevel: action.payload };
-    case getType(actions.updateGlobalSchemaCompatibilityLevelAction.success):
-      return { ...state, globalSchemaCompatibilityLevel: action.payload };
-    default:
-      return state;
-  }
-};
-
-export default reducer;

+ 85 - 0
kafka-ui-react-app/src/redux/reducers/schemas/schemasSlice.ts

@@ -0,0 +1,85 @@
+import {
+  createAsyncThunk,
+  createEntityAdapter,
+  createSelector,
+  createSlice,
+} from '@reduxjs/toolkit';
+import { Configuration, SchemasApi, SchemaSubject } from 'generated-sources';
+import { BASE_PARAMS } from 'lib/constants';
+import { getResponse } from 'lib/errorHandling';
+import { ClusterName, RootState } from 'redux/interfaces';
+import { createFetchingSelector } from 'redux/reducers/loader/selectors';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+export const schemasApiClient = new SchemasApi(apiClientConf);
+
+export const fetchSchemas = createAsyncThunk<SchemaSubject[], ClusterName>(
+  'schemas/fetch',
+  async (clusterName: ClusterName, { rejectWithValue }) => {
+    try {
+      return await schemasApiClient.getSchemas({ clusterName });
+    } catch (error) {
+      return rejectWithValue(await getResponse(error as Response));
+    }
+  }
+);
+export const fetchSchemaVersions = createAsyncThunk<
+  SchemaSubject[],
+  { clusterName: ClusterName; subject: SchemaSubject['subject'] }
+>(
+  'schemas/versions/fetch',
+  async ({ clusterName, subject }, { rejectWithValue }) => {
+    try {
+      return await schemasApiClient.getAllVersionsBySubject({
+        clusterName,
+        subject,
+      });
+    } catch (error) {
+      return rejectWithValue(await getResponse(error as Response));
+    }
+  }
+);
+
+const schemaVersionsAdapter = createEntityAdapter<SchemaSubject>();
+const schemasAdapter = createEntityAdapter<SchemaSubject>({
+  selectId: ({ subject }) => subject,
+});
+
+const schemasSlice = createSlice({
+  name: 'schemas',
+  initialState: schemasAdapter.getInitialState({
+    versions: schemaVersionsAdapter.getInitialState(),
+  }),
+  reducers: {
+    schemaAdded: schemasAdapter.addOne,
+  },
+  extraReducers: (builder) => {
+    builder.addCase(fetchSchemas.fulfilled, (state, { payload }) => {
+      schemasAdapter.setAll(state, payload);
+    });
+    builder.addCase(fetchSchemaVersions.fulfilled, (state, { payload }) => {
+      schemaVersionsAdapter.setAll(state.versions, payload);
+    });
+  },
+});
+
+export const { selectAll: selectAllSchemas, selectById: selectSchemaById } =
+  schemasAdapter.getSelectors<RootState>((state) => state.schemas);
+
+export const { selectAll: selectAllSchemaVersions } =
+  schemaVersionsAdapter.getSelectors<RootState>(
+    (state) => state.schemas.versions
+  );
+
+export const { schemaAdded } = schemasSlice.actions;
+
+export const getAreSchemasFulfilled = createSelector(
+  createFetchingSelector('schemas/fetch'),
+  (status) => status === 'fulfilled'
+);
+export const getAreSchemaVersionsFulfilled = createSelector(
+  createFetchingSelector('schemas/versions/fetch'),
+  (status) => status === 'fulfilled'
+);
+
+export default schemasSlice.reducer;

+ 0 - 71
kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts

@@ -1,71 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-import { orderBy } from 'lodash';
-import { RootState, SchemasState } from 'redux/interfaces';
-import { createLeagcyFetchingSelector } from 'redux/reducers/loader/selectors';
-
-const schemasState = ({ schemas }: RootState): SchemasState => schemas;
-
-const getAllNames = (state: RootState) => schemasState(state).allNames;
-const getSchemaMap = (state: RootState) => schemasState(state).byName;
-export const getGlobalSchemaCompatibilityLevel = (state: RootState) =>
-  schemasState(state).globalSchemaCompatibilityLevel;
-
-const getSchemaListFetchingStatus = createLeagcyFetchingSelector(
-  'GET_CLUSTER_SCHEMAS'
-);
-
-const getSchemaVersionsFetchingStatus = createLeagcyFetchingSelector(
-  'GET_SCHEMA_VERSIONS'
-);
-
-const getSchemaCreationStatus = createLeagcyFetchingSelector('POST_SCHEMA');
-
-const getGlobalSchemaCompatibilityLevelFetchingStatus =
-  createLeagcyFetchingSelector('GET_GLOBAL_SCHEMA_COMPATIBILITY');
-
-export const getIsSchemaListFetched = createSelector(
-  getSchemaListFetchingStatus,
-  (status) => status === 'fetched'
-);
-
-export const getGlobalSchemaCompatibilityLevelFetched = createSelector(
-  getGlobalSchemaCompatibilityLevelFetchingStatus,
-  (status) => status === 'fetched'
-);
-
-export const getIsSchemaListFetching = createSelector(
-  getSchemaListFetchingStatus,
-  (status) => status === 'fetching' || status === 'notFetched'
-);
-
-export const getIsSchemaVersionFetched = createSelector(
-  getSchemaVersionsFetchingStatus,
-  (status) => status === 'fetched'
-);
-
-export const getSchemaCreated = createSelector(
-  getSchemaCreationStatus,
-  (status) => status === 'fetched'
-);
-
-export const getSchemaList = createSelector(
-  getIsSchemaListFetched,
-  getAllNames,
-  getSchemaMap,
-  (isFetched, allNames, byName) =>
-    isFetched ? allNames.map((subject) => byName[subject]) : []
-);
-
-const getSchemaName = (_: RootState, subject: string) => subject;
-
-export const getSchema = createSelector(
-  getSchemaMap,
-  getSchemaName,
-  (schemas, subject) => schemas[subject]
-);
-
-export const getSortedSchemaVersions = createSelector(
-  schemasState,
-  ({ currentSchemaVersions }) =>
-    orderBy(currentSchemaVersions, ['id'], ['desc'])
-);

+ 1 - 7
kafka-ui-react-app/src/redux/reducers/topicMessages/__test__/reducer.spec.ts

@@ -1,20 +1,14 @@
 import {
   addTopicMessage,
-  fetchSchemaVersionsAction,
   resetTopicMessages,
   updateTopicMessagesMeta,
   updateTopicMessagesPhase,
 } from 'redux/actions';
-import reducer, { initialState } from 'redux/reducers/topicMessages/reducer';
+import reducer from 'redux/reducers/topicMessages/reducer';
 
 import { topicMessagePayload, topicMessagesMetaPayload } from './fixtures';
 
 describe('TopicMessages reducer', () => {
-  it('returns the initial state', () => {
-    expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
-      initialState
-    );
-  });
   it('Adds new message', () => {
     const state = reducer(undefined, addTopicMessage(topicMessagePayload));
     expect(state.messages.length).toEqual(1);

+ 4 - 0
kafka-ui-react-app/src/theme/index.scss

@@ -35,6 +35,10 @@ html {
   background-color: #fff;
 }
 
+input, select, textarea, button {
+  font-family: inherit;
+}
+
 code {
   font-family: 'Roboto Mono', sans-serif;
 }

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

@@ -41,6 +41,14 @@ const theme = {
     minWidth: '1200px',
     navBarWidth: '201px',
     navBarHeight: '3.25rem',
+    mainColor: Colors.neutral[5],
+  },
+  panelColor: Colors.neutral[0],
+  headingStyles: {
+    h3: {
+      color: Colors.neutral[50],
+      fontSize: '14px',
+    },
   },
   alert: {
     color: {
@@ -165,7 +173,7 @@ const theme = {
   tagStyles: {
     backgroundColor: {
       green: Colors.green[10],
-      gray: Colors.neutral[10],
+      gray: Colors.neutral[5],
       yellow: Colors.yellow[10],
       white: Colors.neutral[10],
       red: Colors.red[10],
@@ -173,6 +181,8 @@ const theme = {
     color: Colors.neutral[90],
   },
   paginationStyles: {
+    backgroundColor: Colors.neutral[0],
+    currentPage: Colors.neutral[10],
     borderColor: {
       normal: Colors.neutral[30],
       hover: Colors.neutral[50],
@@ -203,6 +213,16 @@ const theme = {
       lightTextColor: Colors.neutral[30],
     },
   },
+  scrollbar: {
+    trackColor: {
+      normal: Colors.neutral[0],
+      active: Colors.neutral[5],
+    },
+    thumbColor: {
+      normal: Colors.neutral[0],
+      active: Colors.neutral[50],
+    },
+  },
 };
 
 export type ThemeType = typeof theme;