Browse Source

Create Schema Registry form (#209)

* First commit

* Create Schema Form. Refactoring

* Specs for Create Schema Registry form created

* Update thunks.spec.ts

* Update actions.spec.ts

Co-authored-by: Oleg Shuralev <workshur@gmail.com>
Guzel738 4 years ago
parent
commit
44cf449a8f
31 changed files with 656 additions and 261 deletions
  1. 3 3
      kafka-ui-react-app/package-lock.json
  2. 1 1
      kafka-ui-react-app/package.json
  3. 0 8
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  4. 0 32
      kafka-ui-react-app/src/components/Schemas/Details/__test__/__snapshots__/Details.spec.tsx.snap
  5. 27 4
      kafka-ui-react-app/src/components/Schemas/List/List.tsx
  6. 1 2
      kafka-ui-react-app/src/components/Schemas/List/ListContainer.tsx
  7. 9 12
      kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx
  8. 0 102
      kafka-ui-react-app/src/components/Schemas/List/__test__/__snapshots__/List.spec.tsx.snap
  9. 2 2
      kafka-ui-react-app/src/components/Schemas/List/__test__/fixtures.ts
  10. 115 0
      kafka-ui-react-app/src/components/Schemas/New/New.tsx
  11. 15 0
      kafka-ui-react-app/src/components/Schemas/New/NewContainer.ts
  12. 37 0
      kafka-ui-react-app/src/components/Schemas/New/__test__/New.spec.tsx
  13. 189 0
      kafka-ui-react-app/src/components/Schemas/New/__test__/__snapshots__/New.spec.tsx.snap
  14. 27 21
      kafka-ui-react-app/src/components/Schemas/Schemas.tsx
  15. 5 22
      kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx
  16. 11 26
      kafka-ui-react-app/src/components/Schemas/__test__/Schemas.spec.tsx
  17. 0 18
      kafka-ui-react-app/src/components/Schemas/__test__/__snapshots__/Schemas.spec.tsx.snap
  18. 1 0
      kafka-ui-react-app/src/lib/constants.ts
  19. 8 1
      kafka-ui-react-app/src/lib/paths.ts
  20. 26 3
      kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts
  21. 6 1
      kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts
  22. 32 0
      kafka-ui-react-app/src/redux/actions/__test__/thunks.spec.ts
  23. 6 0
      kafka-ui-react-app/src/redux/actions/actions.ts
  24. 21 0
      kafka-ui-react-app/src/redux/actions/thunks.ts
  25. 5 1
      kafka-ui-react-app/src/redux/interfaces/schema.ts
  26. 18 0
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/__snapshots__/reducer.spec.ts.snap
  27. 44 0
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts
  28. 10 0
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/reducer.spec.ts
  29. 11 2
      kafka-ui-react-app/src/redux/reducers/schemas/__test__/selectors.spec.ts
  30. 14 0
      kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts
  31. 12 0
      kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts

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

@@ -15794,9 +15794,9 @@
       "dev": true
       "dev": true
     },
     },
     "react-hook-form": {
     "react-hook-form": {
-      "version": "6.15.1",
-      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.1.tgz",
-      "integrity": "sha512-bL0LQuQ3OlM3JYfbacKtBPLOHhmgYz8Lj6ivMrvu2M6e1wnt4sbGRtPEPYCc/8z3WDbjrMwfAfLX92OsB65pFA=="
+      "version": "6.15.4",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz",
+      "integrity": "sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w=="
     },
     },
     "react-is": {
     "react-is": {
       "version": "17.0.1",
       "version": "17.0.1",

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

@@ -17,7 +17,7 @@
     "react": "^17.0.1",
     "react": "^17.0.1",
     "react-datepicker": "^3.5.0",
     "react-datepicker": "^3.5.0",
     "react-dom": "^17.0.1",
     "react-dom": "^17.0.1",
-    "react-hook-form": "^6.15.1",
+    "react-hook-form": "^6.15.4",
     "react-json-tree": "^0.13.0",
     "react-json-tree": "^0.13.0",
     "react-multi-select-component": "^2.0.14",
     "react-multi-select-component": "^2.0.14",
     "react-redux": "^7.2.2",
     "react-redux": "^7.2.2",

+ 0 - 8
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -55,14 +55,6 @@ const Details: React.FC<DetailsProps> = ({
             </div>
             </div>
           </div>
           </div>
           <div className="level-right">
           <div className="level-right">
-            <button
-              className="button is-primary is-small level-item"
-              type="button"
-              title="in development"
-              disabled
-            >
-              Create Schema
-            </button>
             <button
             <button
               className="button is-warning is-small level-item"
               className="button is-warning is-small level-item"
               type="button"
               type="button"

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

@@ -51,14 +51,6 @@ exports[`Details View Initial state matches snapshot 1`] = `
       <div
       <div
         className="level-right"
         className="level-right"
       >
       >
-        <button
-          className="button is-primary is-small level-item"
-          disabled={true}
-          title="in development"
-          type="button"
-        >
-          Create Schema
-        </button>
         <button
         <button
           className="button is-warning is-small level-item"
           className="button is-warning is-small level-item"
           disabled={true}
           disabled={true}
@@ -165,14 +157,6 @@ exports[`Details View when page with schema versions is loading matches snapshot
       <div
       <div
         className="level-right"
         className="level-right"
       >
       >
-        <button
-          className="button is-primary is-small level-item"
-          disabled={true}
-          title="in development"
-          type="button"
-        >
-          Create Schema
-        </button>
         <button
         <button
           className="button is-warning is-small level-item"
           className="button is-warning is-small level-item"
           disabled={true}
           disabled={true}
@@ -258,14 +242,6 @@ exports[`Details View when page with schema versions loaded when schema has vers
       <div
       <div
         className="level-right"
         className="level-right"
       >
       >
-        <button
-          className="button is-primary is-small level-item"
-          disabled={true}
-          title="in development"
-          type="button"
-        >
-          Create Schema
-        </button>
         <button
         <button
           className="button is-warning is-small level-item"
           className="button is-warning is-small level-item"
           disabled={true}
           disabled={true}
@@ -397,14 +373,6 @@ exports[`Details View when page with schema versions loaded when versions are em
       <div
       <div
         className="level-right"
         className="level-right"
       >
       >
-        <button
-          className="button is-primary is-small level-item"
-          disabled={true}
-          title="in development"
-          type="button"
-        >
-          Create Schema
-        </button>
         <button
         <button
           className="button is-warning is-small level-item"
           className="button is-warning is-small level-item"
           disabled={true}
           disabled={true}

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

@@ -1,6 +1,8 @@
 import React from 'react';
 import React from 'react';
 import { SchemaSubject } from 'generated-sources';
 import { SchemaSubject } from 'generated-sources';
-import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
+import { NavLink, useParams } from 'react-router-dom';
+import { clusterSchemaNewPath } from 'lib/paths';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import ListItem from './ListItem';
 import ListItem from './ListItem';
 
 
 export interface ListProps {
 export interface ListProps {
@@ -8,9 +10,24 @@ export interface ListProps {
 }
 }
 
 
 const List: React.FC<ListProps> = ({ schemas }) => {
 const List: React.FC<ListProps> = ({ schemas }) => {
+  const { clusterName } = useParams<{ clusterName: string }>();
+
   return (
   return (
     <div className="section">
     <div className="section">
       <Breadcrumb>Schema Registry</Breadcrumb>
       <Breadcrumb>Schema Registry</Breadcrumb>
+      <div className="box">
+        <div className="level">
+          <div className="level-item level-right">
+            <NavLink
+              className="button is-primary"
+              to={clusterSchemaNewPath(clusterName)}
+            >
+              Create Schema
+            </NavLink>
+          </div>
+        </div>
+      </div>
+
       <div className="box">
       <div className="box">
         <table className="table is-striped is-fullwidth">
         <table className="table is-striped is-fullwidth">
           <thead>
           <thead>
@@ -21,9 +38,15 @@ const List: React.FC<ListProps> = ({ schemas }) => {
             </tr>
             </tr>
           </thead>
           </thead>
           <tbody>
           <tbody>
-            {schemas.map((subject) => (
-              <ListItem key={subject.id} subject={subject} />
-            ))}
+            {schemas.length > 0 ? (
+              schemas.map((subject) => (
+                <ListItem key={subject.id} subject={subject} />
+              ))
+            ) : (
+              <tr>
+                <td colSpan={10}>No schemas found</td>
+              </tr>
+            )}
           </tbody>
           </tbody>
         </table>
         </table>
       </div>
       </div>

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

@@ -1,6 +1,5 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 import { RootState } from 'redux/interfaces';
 import { RootState } from 'redux/interfaces';
-import { withRouter } from 'react-router-dom';
 import { getSchemaList } from 'redux/reducers/schemas/selectors';
 import { getSchemaList } from 'redux/reducers/schemas/selectors';
 import List from './List';
 import List from './List';
 
 
@@ -8,4 +7,4 @@ const mapStateToProps = (state: RootState) => ({
   schemas: getSchemaList(state),
   schemas: getSchemaList(state),
 });
 });
 
 
-export default withRouter(connect(mapStateToProps)(List));
+export default connect(mapStateToProps)(List);

+ 9 - 12
kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
+import { mount, shallow } from 'enzyme';
 import { Provider } from 'react-redux';
 import { Provider } from 'react-redux';
-import { shallow } from 'enzyme';
+import { StaticRouter } from 'react-router';
 import configureStore from 'redux/store/configureStore';
 import configureStore from 'redux/store/configureStore';
 import ListContainer from '../ListContainer';
 import ListContainer from '../ListContainer';
 import List, { ListProps } from '../List';
 import List, { ListProps } from '../List';
@@ -22,35 +23,31 @@ describe('List', () => {
   });
   });
 
 
   describe('View', () => {
   describe('View', () => {
+    const pathname = `/ui/clusters/clusterName/schemas`;
+
     const setupWrapper = (props: Partial<ListProps> = {}) => (
     const setupWrapper = (props: Partial<ListProps> = {}) => (
-      <List schemas={[]} {...props} />
+      <StaticRouter location={{ pathname }} context={{}}>
+        <List schemas={[]} {...props} />
+      </StaticRouter>
     );
     );
 
 
     describe('without schemas', () => {
     describe('without schemas', () => {
       it('renders table heading without ListItem', () => {
       it('renders table heading without ListItem', () => {
-        const wrapper = shallow(setupWrapper());
+        const wrapper = mount(setupWrapper());
         expect(wrapper.exists('Breadcrumb')).toBeTruthy();
         expect(wrapper.exists('Breadcrumb')).toBeTruthy();
         expect(wrapper.exists('thead')).toBeTruthy();
         expect(wrapper.exists('thead')).toBeTruthy();
         expect(wrapper.exists('ListItem')).toBeFalsy();
         expect(wrapper.exists('ListItem')).toBeFalsy();
       });
       });
-
-      it('matches snapshot', () => {
-        expect(shallow(setupWrapper())).toMatchSnapshot();
-      });
     });
     });
 
 
     describe('with schemas', () => {
     describe('with schemas', () => {
-      const wrapper = shallow(setupWrapper({ schemas }));
+      const wrapper = mount(setupWrapper({ schemas }));
 
 
       it('renders table heading with ListItem', () => {
       it('renders table heading with ListItem', () => {
         expect(wrapper.exists('Breadcrumb')).toBeTruthy();
         expect(wrapper.exists('Breadcrumb')).toBeTruthy();
         expect(wrapper.exists('thead')).toBeTruthy();
         expect(wrapper.exists('thead')).toBeTruthy();
         expect(wrapper.find('ListItem').length).toEqual(3);
         expect(wrapper.find('ListItem').length).toEqual(3);
       });
       });
-
-      it('matches snapshot', () => {
-        expect(shallow(setupWrapper({ schemas }))).toMatchSnapshot();
-      });
     });
     });
   });
   });
 });
 });

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

@@ -1,102 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`List View with schemas matches snapshot 1`] = `
-<div
-  className="section"
->
-  <Breadcrumb>
-    Schema Registry
-  </Breadcrumb>
-  <div
-    className="box"
-  >
-    <table
-      className="table is-striped is-fullwidth"
-    >
-      <thead>
-        <tr>
-          <th>
-            Schema Name
-          </th>
-          <th>
-            Version
-          </th>
-          <th>
-            Compatibility
-          </th>
-        </tr>
-      </thead>
-      <tbody>
-        <ListItem
-          key="1"
-          subject={
-            Object {
-              "compatibilityLevel": "BACKWARD",
-              "id": 1,
-              "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-              "subject": "test",
-              "version": "1",
-            }
-          }
-        />
-        <ListItem
-          key="1"
-          subject={
-            Object {
-              "compatibilityLevel": "BACKWARD",
-              "id": 1,
-              "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-              "subject": "test2",
-              "version": "1",
-            }
-          }
-        />
-        <ListItem
-          key="1"
-          subject={
-            Object {
-              "compatibilityLevel": "BACKWARD",
-              "id": 1,
-              "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord3\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
-              "subject": "test3",
-              "version": "1",
-            }
-          }
-        />
-      </tbody>
-    </table>
-  </div>
-</div>
-`;
-
-exports[`List View without schemas matches snapshot 1`] = `
-<div
-  className="section"
->
-  <Breadcrumb>
-    Schema Registry
-  </Breadcrumb>
-  <div
-    className="box"
-  >
-    <table
-      className="table is-striped is-fullwidth"
-    >
-      <thead>
-        <tr>
-          <th>
-            Schema Name
-          </th>
-          <th>
-            Version
-          </th>
-          <th>
-            Compatibility
-          </th>
-        </tr>
-      </thead>
-      <tbody />
-    </table>
-  </div>
-</div>
-`;

+ 2 - 2
kafka-ui-react-app/src/components/Schemas/List/__test__/fixtures.ts

@@ -12,7 +12,7 @@ export const schemas: SchemaSubject[] = [
   {
   {
     subject: 'test2',
     subject: 'test2',
     version: '1',
     version: '1',
-    id: 1,
+    id: 2,
     schema:
     schema:
       '{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
       '{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
     compatibilityLevel: 'BACKWARD',
     compatibilityLevel: 'BACKWARD',
@@ -20,7 +20,7 @@ export const schemas: SchemaSubject[] = [
   {
   {
     subject: 'test3',
     subject: 'test3',
     version: '1',
     version: '1',
-    id: 1,
+    id: 12,
     schema:
     schema:
       '{"type":"record","name":"MyRecord3","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
       '{"type":"record","name":"MyRecord3","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
     compatibilityLevel: 'BACKWARD',
     compatibilityLevel: 'BACKWARD',

+ 115 - 0
kafka-ui-react-app/src/components/Schemas/New/New.tsx

@@ -0,0 +1,115 @@
+import React from 'react';
+import { ClusterName, SchemaName, NewSchemaSubjectRaw } from 'redux/interfaces';
+import { useForm } from 'react-hook-form';
+import { ErrorMessage } from '@hookform/error-message';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+import { clusterSchemaPath, clusterSchemasPath } from 'lib/paths';
+import { NewSchemaSubject } from 'generated-sources';
+import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants';
+import { useHistory, useParams } from 'react-router';
+
+export interface NewProps {
+  createSchema: (
+    clusterName: ClusterName,
+    subject: SchemaName,
+    newSchemaSubject: NewSchemaSubject
+  ) => void;
+}
+
+const New: React.FC<NewProps> = ({ createSchema }) => {
+  const { clusterName } = useParams<{ clusterName: string }>();
+  const history = useHistory();
+  const {
+    register,
+    errors,
+    handleSubmit,
+    formState: { isDirty, isSubmitting },
+  } = useForm<NewSchemaSubjectRaw>();
+
+  const onSubmit = React.useCallback(
+    async ({ subject, schema }: NewSchemaSubjectRaw) => {
+      try {
+        await createSchema(clusterName, subject, { schema });
+        history.push(clusterSchemaPath(clusterName, subject));
+      } catch (e) {
+        // Show Error
+      }
+    },
+    [clusterName]
+  );
+
+  return (
+    <div className="section">
+      <div className="level">
+        <div className="level-item level-left">
+          <Breadcrumb
+            links={[
+              {
+                href: clusterSchemasPath(clusterName),
+                label: 'Schema Registry',
+              },
+            ]}
+          >
+            New Schema
+          </Breadcrumb>
+        </div>
+      </div>
+
+      <div className="box">
+        <form onSubmit={handleSubmit(onSubmit)}>
+          <div>
+            <div className="field">
+              <label className="label">Subject *</label>
+              <div className="control">
+                <input
+                  className="input"
+                  placeholder="Schema Name"
+                  ref={register({
+                    required: 'Topic Name is required.',
+                    pattern: {
+                      value: SCHEMA_NAME_VALIDATION_PATTERN,
+                      message: 'Only alphanumeric, _, -, and . allowed',
+                    },
+                  })}
+                  name="subject"
+                  autoComplete="off"
+                  disabled={isSubmitting}
+                />
+              </div>
+              <p className="help is-danger">
+                <ErrorMessage errors={errors} name="subject" />
+              </p>
+            </div>
+
+            <div className="field">
+              <label className="label">Schema *</label>
+              <div className="control">
+                <textarea
+                  className="textarea"
+                  ref={register}
+                  name="schema"
+                  disabled={isSubmitting}
+                />
+              </div>
+              <p className="help is-danger">
+                <ErrorMessage errors={errors} name="schema" />
+              </p>
+            </div>
+          </div>
+          <br />
+          <div className="field">
+            <div className="control">
+              <input
+                type="submit"
+                className="button is-primary"
+                disabled={isSubmitting || !isDirty}
+              />
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+};
+
+export default New;

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

@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import { RootState } from 'redux/interfaces';
+import { createSchema } from 'redux/actions';
+import { getSchemaCreated } from 'redux/reducers/schemas/selectors';
+import New from './New';
+
+const mapStateToProps = (state: RootState) => ({
+  isSchemaCreated: getSchemaCreated(state),
+});
+
+const mapDispatchToProps = {
+  createSchema,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(New);

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

@@ -0,0 +1,37 @@
+import React from 'react';
+import configureStore from 'redux/store/configureStore';
+import { mount, shallow } from 'enzyme';
+import { Provider } from 'react-redux';
+import { StaticRouter } from 'react-router-dom';
+import NewContainer from '../NewContainer';
+import New, { NewProps } from '../New';
+
+describe('New', () => {
+  describe('Container', () => {
+    const store = configureStore();
+
+    it('renders view', () => {
+      const component = shallow(
+        <Provider store={store}>
+          <NewContainer />
+        </Provider>
+      );
+
+      expect(component.exists()).toBeTruthy();
+    });
+  });
+
+  describe('View', () => {
+    const pathname = '/ui/clusters/clusterName/schemas/new';
+
+    const setupWrapper = (props: Partial<NewProps> = {}) => (
+      <StaticRouter location={{ pathname }} context={{}}>
+        <New createSchema={jest.fn()} {...props} />
+      </StaticRouter>
+    );
+
+    it('matches snapshot', () => {
+      expect(mount(setupWrapper())).toMatchSnapshot();
+    });
+  });
+});

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

@@ -0,0 +1,189 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`New View matches snapshot 1`] = `
+<StaticRouter
+  context={Object {}}
+  location={
+    Object {
+      "pathname": "/ui/clusters/clusterName/schemas/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/new",
+          "search": "",
+        },
+        "push": [Function],
+        "replace": [Function],
+      }
+    }
+    staticContext={Object {}}
+  >
+    <New
+      createSchema={[MockFunction]}
+    >
+      <div
+        className="section"
+      >
+        <div
+          className="level"
+        >
+          <div
+            className="level-item level-left"
+          >
+            <Breadcrumb
+              links={
+                Array [
+                  Object {
+                    "href": "/ui/clusters/undefined/schemas",
+                    "label": "Schema Registry",
+                  },
+                ]
+              }
+            >
+              <nav
+                aria-label="breadcrumbs"
+                className="breadcrumb"
+              >
+                <ul>
+                  <li
+                    key="/ui/clusters/undefined/schemas"
+                  >
+                    <NavLink
+                      to="/ui/clusters/undefined/schemas"
+                    >
+                      <Link
+                        aria-current={null}
+                        to={
+                          Object {
+                            "hash": "",
+                            "pathname": "/ui/clusters/undefined/schemas",
+                            "search": "",
+                            "state": null,
+                          }
+                        }
+                      >
+                        <LinkAnchor
+                          aria-current={null}
+                          href="/ui/clusters/undefined/schemas"
+                          navigate={[Function]}
+                        >
+                          <a
+                            aria-current={null}
+                            href="/ui/clusters/undefined/schemas"
+                            onClick={[Function]}
+                          >
+                            Schema Registry
+                          </a>
+                        </LinkAnchor>
+                      </Link>
+                    </NavLink>
+                  </li>
+                  <li
+                    className="is-active"
+                  >
+                    <span
+                      className=""
+                    >
+                      New Schema
+                    </span>
+                  </li>
+                </ul>
+              </nav>
+            </Breadcrumb>
+          </div>
+        </div>
+        <div
+          className="box"
+        >
+          <form
+            onSubmit={[Function]}
+          >
+            <div>
+              <div
+                className="field"
+              >
+                <label
+                  className="label"
+                >
+                  Subject *
+                </label>
+                <div
+                  className="control"
+                >
+                  <input
+                    autoComplete="off"
+                    className="input"
+                    disabled={false}
+                    name="subject"
+                    placeholder="Schema Name"
+                  />
+                </div>
+                <p
+                  className="help is-danger"
+                >
+                  <Component
+                    errors={Object {}}
+                    name="subject"
+                  />
+                </p>
+              </div>
+              <div
+                className="field"
+              >
+                <label
+                  className="label"
+                >
+                  Schema *
+                </label>
+                <div
+                  className="control"
+                >
+                  <textarea
+                    className="textarea"
+                    disabled={false}
+                    name="schema"
+                  />
+                </div>
+                <p
+                  className="help is-danger"
+                >
+                  <Component
+                    errors={Object {}}
+                    name="schema"
+                  />
+                </p>
+              </div>
+            </div>
+            <br />
+            <div
+              className="field"
+            >
+              <div
+                className="control"
+              >
+                <input
+                  className="button is-primary"
+                  disabled={true}
+                  type="submit"
+                />
+              </div>
+            </div>
+          </form>
+        </div>
+      </div>
+    </New>
+  </Router>
+</StaticRouter>
+`;

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

@@ -1,43 +1,49 @@
 import React from 'react';
 import React from 'react';
 import { ClusterName } from 'redux/interfaces';
 import { ClusterName } from 'redux/interfaces';
-import { Switch, Route } from 'react-router-dom';
+import { Switch, Route, useParams } from 'react-router-dom';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import ListContainer from './List/ListContainer';
 import ListContainer from './List/ListContainer';
 import DetailsContainer from './Details/DetailsContainer';
 import DetailsContainer from './Details/DetailsContainer';
+import NewContainer from './New/NewContainer';
 
 
 export interface SchemasProps {
 export interface SchemasProps {
-  isFetched: boolean;
-  clusterName: ClusterName;
+  isFetching: boolean;
   fetchSchemasByClusterName: (clusterName: ClusterName) => void;
   fetchSchemasByClusterName: (clusterName: ClusterName) => void;
 }
 }
 
 
 const Schemas: React.FC<SchemasProps> = ({
 const Schemas: React.FC<SchemasProps> = ({
-  isFetched,
+  isFetching,
   fetchSchemasByClusterName,
   fetchSchemasByClusterName,
-  clusterName,
 }) => {
 }) => {
+  const { clusterName } = useParams<{ clusterName: string }>();
+
   React.useEffect(() => {
   React.useEffect(() => {
     fetchSchemasByClusterName(clusterName);
     fetchSchemasByClusterName(clusterName);
   }, [fetchSchemasByClusterName, clusterName]);
   }, [fetchSchemasByClusterName, clusterName]);
 
 
-  if (isFetched) {
-    return (
-      <Switch>
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/schemas"
-          component={ListContainer}
-        />
-        <Route
-          exact
-          path="/ui/clusters/:clusterName/schemas/:subject/latest"
-          component={DetailsContainer}
-        />
-      </Switch>
-    );
+  if (isFetching) {
+    return <PageLoader />;
   }
   }
 
 
-  return <PageLoader />;
+  return (
+    <Switch>
+      <Route
+        exact
+        path="/ui/clusters/:clusterName/schemas"
+        component={ListContainer}
+      />
+      <Route
+        exact
+        path="/ui/clusters/:clusterName/schemas/new"
+        component={NewContainer}
+      />
+      <Route
+        exact
+        path="/ui/clusters/:clusterName/schemas/:subject/latest"
+        component={DetailsContainer}
+      />
+    </Switch>
+  );
 };
 };
 
 
 export default Schemas;
 export default Schemas;

+ 5 - 22
kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx

@@ -1,32 +1,15 @@
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import { ClusterName, RootState } from 'redux/interfaces';
+import { RootState } from 'redux/interfaces';
 import { fetchSchemasByClusterName } from 'redux/actions';
 import { fetchSchemasByClusterName } from 'redux/actions';
-import { getIsSchemaListFetched } from 'redux/reducers/schemas/selectors';
-import { RouteComponentProps, withRouter } from 'react-router-dom';
+import { getIsSchemaListFetching } from 'redux/reducers/schemas/selectors';
 import Schemas from './Schemas';
 import Schemas from './Schemas';
 
 
-interface RouteProps {
-  clusterName: ClusterName;
-}
-
-type OwnProps = RouteComponentProps<RouteProps>;
-
-const mapStateToProps = (
-  state: RootState,
-  {
-    match: {
-      params: { clusterName },
-    },
-  }: OwnProps
-) => ({
-  isFetched: getIsSchemaListFetched(state),
-  clusterName,
+const mapStateToProps = (state: RootState) => ({
+  isFetching: getIsSchemaListFetching(state),
 });
 });
 
 
 const mapDispatchToProps = {
 const mapDispatchToProps = {
   fetchSchemasByClusterName,
   fetchSchemasByClusterName,
 };
 };
 
 
-export default withRouter(
-  connect(mapStateToProps, mapDispatchToProps)(Schemas)
-);
+export default connect(mapStateToProps, mapDispatchToProps)(Schemas);

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

@@ -1,10 +1,8 @@
 import React from 'react';
 import React from 'react';
 import { Provider } from 'react-redux';
 import { Provider } from 'react-redux';
-import { shallow } from 'enzyme';
+import { mount } from 'enzyme';
 import configureStore from 'redux/store/configureStore';
 import configureStore from 'redux/store/configureStore';
 import { StaticRouter } from 'react-router-dom';
 import { StaticRouter } from 'react-router-dom';
-import { match } from 'react-router';
-import { ClusterName } from 'redux/interfaces';
 import Schemas, { SchemasProps } from '../Schemas';
 import Schemas, { SchemasProps } from '../Schemas';
 import SchemasContainer from '../SchemasContainer';
 import SchemasContainer from '../SchemasContainer';
 
 
@@ -15,7 +13,7 @@ describe('Schemas', () => {
     const store = configureStore();
     const store = configureStore();
 
 
     it('renders view', () => {
     it('renders view', () => {
-      const component = shallow(
+      const component = mount(
         <Provider store={store}>
         <Provider store={store}>
           <StaticRouter location={{ pathname }} context={{}}>
           <StaticRouter location={{ pathname }} context={{}}>
             <SchemasContainer />
             <SchemasContainer />
@@ -28,12 +26,13 @@ describe('Schemas', () => {
 
 
     describe('View', () => {
     describe('View', () => {
       const setupWrapper = (props: Partial<SchemasProps> = {}) => (
       const setupWrapper = (props: Partial<SchemasProps> = {}) => (
-        <Schemas
-          isFetched
-          clusterName="Test"
-          fetchSchemasByClusterName={jest.fn()}
-          {...props}
-        />
+        <StaticRouter location={{ pathname }} context={{}}>
+          <Schemas
+            isFetching
+            fetchSchemasByClusterName={jest.fn()}
+            {...props}
+          />
+        </StaticRouter>
       );
       );
       describe('Initial state', () => {
       describe('Initial state', () => {
         let useEffect: jest.SpyInstance<
         let useEffect: jest.SpyInstance<
@@ -43,7 +42,6 @@ describe('Schemas', () => {
             deps?: React.DependencyList | undefined
             deps?: React.DependencyList | undefined
           ]
           ]
         >;
         >;
-        let wrapper;
         const mockedFn = jest.fn();
         const mockedFn = jest.fn();
 
 
         const mockedUseEffect = () => {
         const mockedUseEffect = () => {
@@ -53,33 +51,20 @@ describe('Schemas', () => {
         beforeEach(() => {
         beforeEach(() => {
           useEffect = jest.spyOn(React, 'useEffect');
           useEffect = jest.spyOn(React, 'useEffect');
           mockedUseEffect();
           mockedUseEffect();
-
-          wrapper = shallow(
-            setupWrapper({ fetchSchemasByClusterName: mockedFn })
-          );
         });
         });
 
 
         it('should call fetchSchemasByClusterName every render', () => {
         it('should call fetchSchemasByClusterName every render', () => {
+          mount(setupWrapper({ fetchSchemasByClusterName: mockedFn }));
           expect(mockedFn).toHaveBeenCalled();
           expect(mockedFn).toHaveBeenCalled();
         });
         });
-
-        it('matches snapshot', () => {
-          expect(
-            shallow(setupWrapper({ fetchSchemasByClusterName: mockedFn }))
-          ).toMatchSnapshot();
-        });
       });
       });
 
 
       describe('when page is loading', () => {
       describe('when page is loading', () => {
-        const wrapper = shallow(setupWrapper({ isFetched: false }));
+        const wrapper = mount(setupWrapper({ isFetching: true }));
 
 
         it('renders PageLoader', () => {
         it('renders PageLoader', () => {
           expect(wrapper.exists('PageLoader')).toBeTruthy();
           expect(wrapper.exists('PageLoader')).toBeTruthy();
         });
         });
-
-        it('matches snapshot', () => {
-          expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
-        });
       });
       });
     });
     });
   });
   });

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

@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Schemas Container View Initial state matches snapshot 1`] = `
-<Switch>
-  <Route
-    component={[Function]}
-    exact={true}
-    path="/ui/clusters/:clusterName/schemas"
-  />
-  <Route
-    component={[Function]}
-    exact={true}
-    path="/ui/clusters/:clusterName/schemas/:subject/latest"
-  />
-</Switch>
-`;
-
-exports[`Schemas Container View when page is loading matches snapshot 1`] = `<PageLoader />`;

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

@@ -9,6 +9,7 @@ export const BASE_PARAMS: ConfigurationParameters = {
 };
 };
 
 
 export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
 export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
+export const SCHEMA_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
 
 
 export const MILLISECONDS_IN_WEEK = 604_800_000;
 export const MILLISECONDS_IN_WEEK = 604_800_000;
 export const MILLISECONDS_IN_DAY = 86_400_000;
 export const MILLISECONDS_IN_DAY = 86_400_000;

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

@@ -1,4 +1,4 @@
-import { ClusterName, TopicName } from 'redux/interfaces';
+import { ClusterName, SchemaName, TopicName } from 'redux/interfaces';
 
 
 const clusterPath = (clusterName: ClusterName) => `/ui/clusters/${clusterName}`;
 const clusterPath = (clusterName: ClusterName) => `/ui/clusters/${clusterName}`;
 
 
@@ -12,6 +12,8 @@ export const clusterConsumerGroupsPath = (clusterName: ClusterName) =>
   `${clusterPath(clusterName)}/consumer-groups`;
   `${clusterPath(clusterName)}/consumer-groups`;
 export const clusterSchemasPath = (clusterName: ClusterName) =>
 export const clusterSchemasPath = (clusterName: ClusterName) =>
   `${clusterPath(clusterName)}/schemas`;
   `${clusterPath(clusterName)}/schemas`;
+export const clusterSchemaNewPath = (clusterName: ClusterName) =>
+  `${clusterPath(clusterName)}/schemas/new`;
 
 
 export const clusterTopicPath = (
 export const clusterTopicPath = (
   clusterName: ClusterName,
   clusterName: ClusterName,
@@ -30,3 +32,8 @@ export const clusterTopicsTopicEditPath = (
   clusterName: ClusterName,
   clusterName: ClusterName,
   topicName: TopicName
   topicName: TopicName
 ) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
 ) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
+
+export const clusterSchemaPath = (
+  clusterName: ClusterName,
+  subject: SchemaName
+) => `${clusterSchemasPath(clusterName)}/${subject}/latest`;

+ 26 - 3
kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts

@@ -6,13 +6,13 @@ import * as actions from '../actions';
 
 
 describe('Actions', () => {
 describe('Actions', () => {
   describe('fetchClusterStatsAction', () => {
   describe('fetchClusterStatsAction', () => {
-    it('creates an REQUEST action', () => {
+    it('creates a REQUEST action', () => {
       expect(actions.fetchClusterStatsAction.request()).toEqual({
       expect(actions.fetchClusterStatsAction.request()).toEqual({
         type: 'GET_CLUSTER_STATUS__REQUEST',
         type: 'GET_CLUSTER_STATUS__REQUEST',
       });
       });
     });
     });
 
 
-    it('creates an SUCCESS action', () => {
+    it('creates a SUCCESS action', () => {
       expect(
       expect(
         actions.fetchClusterStatsAction.success({ brokerCount: 1 })
         actions.fetchClusterStatsAction.success({ brokerCount: 1 })
       ).toEqual({
       ).toEqual({
@@ -23,7 +23,7 @@ describe('Actions', () => {
       });
       });
     });
     });
 
 
-    it('creates an FAILURE action', () => {
+    it('creates a FAILURE action', () => {
       expect(actions.fetchClusterStatsAction.failure()).toEqual({
       expect(actions.fetchClusterStatsAction.failure()).toEqual({
         type: 'GET_CLUSTER_STATUS__FAILURE',
         type: 'GET_CLUSTER_STATUS__FAILURE',
       });
       });
@@ -75,4 +75,27 @@ describe('Actions', () => {
       });
       });
     });
     });
   });
   });
+
+  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',
+      });
+    });
+  });
 });
 });

+ 6 - 1
kafka-ui-react-app/src/redux/actions/__test__/fixtures.ts

@@ -1,4 +1,4 @@
-import { ClusterStats } from 'generated-sources';
+import { ClusterStats, NewSchemaSubject } from 'generated-sources';
 
 
 export const clusterStats: ClusterStats = {
 export const clusterStats: ClusterStats = {
   brokerCount: 1,
   brokerCount: 1,
@@ -11,3 +11,8 @@ export const clusterStats: ClusterStats = {
   underReplicatedPartitionCount: 0,
   underReplicatedPartitionCount: 0,
   diskUsage: [{ brokerId: 1, segmentSize: 6538, segmentCount: 6 }],
   diskUsage: [{ brokerId: 1, segmentSize: 6538, segmentCount: 6 }],
 };
 };
+
+export const schemaPayload: NewSchemaSubject = {
+  schema:
+    '{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
+};

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

@@ -105,4 +105,36 @@ describe('Thunks', () => {
       ]);
       ]);
     });
     });
   });
   });
+
+  describe('createSchema', () => {
+    it('creates POST_SCHEMA__SUCCESS when posting new schema', async () => {
+      fetchMock.postOnce(`/api/clusters/${clusterName}/schemas/${subject}`, {
+        body: schemaFixtures.schemaVersionsPayload[0],
+      });
+      await store.dispatch(
+        thunks.createSchema(clusterName, subject, 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/${subject}`,
+    //     404
+    //   );
+    //   await store.dispatch(
+    //     thunks.createSchema(clusterName, subject, fixtures.schemaPayload)
+    //   );
+    //   expect(store.getActions()).toEqual([
+    //     actions.createSchemaAction.request(),
+    //     actions.createSchemaAction.failure(),
+    //   ]);
+    //   expect(store.getActions()).toThrow();
+    // });
+  });
 });
 });

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

@@ -109,3 +109,9 @@ export const fetchSchemaVersionsAction = createAsyncAction(
   'GET_SCHEMA_VERSIONS__SUCCESS',
   'GET_SCHEMA_VERSIONS__SUCCESS',
   'GET_SCHEMA_VERSIONS__FAILURE'
   'GET_SCHEMA_VERSIONS__FAILURE'
 )<undefined, SchemaSubject[], undefined>();
 )<undefined, SchemaSubject[], undefined>();
+
+export const createSchemaAction = createAsyncAction(
+  'POST_SCHEMA__REQUEST',
+  'POST_SCHEMA__SUCCESS',
+  'POST_SCHEMA__FAILURE'
+)<undefined, SchemaSubject, undefined>();

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

@@ -5,6 +5,8 @@ import {
   Topic,
   Topic,
   TopicFormData,
   TopicFormData,
   TopicConfig,
   TopicConfig,
+  NewSchemaSubject,
+  SchemaSubject,
 } from 'generated-sources';
 } from 'generated-sources';
 import {
 import {
   ConsumerGroupID,
   ConsumerGroupID,
@@ -280,3 +282,22 @@ export const fetchSchemaVersions = (
     dispatch(actions.fetchSchemaVersionsAction.failure());
     dispatch(actions.fetchSchemaVersionsAction.failure());
   }
   }
 };
 };
+
+export const createSchema = (
+  clusterName: ClusterName,
+  subject: SchemaName,
+  newSchemaSubject: NewSchemaSubject
+): PromiseThunkResult => async (dispatch) => {
+  dispatch(actions.createSchemaAction.request());
+  try {
+    const schema: SchemaSubject = await apiClient.createNewSchema({
+      clusterName,
+      subject,
+      newSchemaSubject,
+    });
+    dispatch(actions.createSchemaAction.success(schema));
+  } catch (e) {
+    dispatch(actions.createSchemaAction.failure());
+    throw e;
+  }
+};

+ 5 - 1
kafka-ui-react-app/src/redux/interfaces/schema.ts

@@ -1,4 +1,4 @@
-import { SchemaSubject } from 'generated-sources';
+import { NewSchemaSubject, SchemaSubject } from 'generated-sources';
 
 
 export type SchemaName = string;
 export type SchemaName = string;
 
 
@@ -7,3 +7,7 @@ export interface SchemasState {
   allNames: SchemaName[];
   allNames: SchemaName[];
   currentSchemaVersions: SchemaSubject[];
   currentSchemaVersions: SchemaSubject[];
 }
 }
+
+export interface NewSchemaSubjectRaw extends NewSchemaSubject {
+  subject: string;
+}

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

@@ -56,3 +56,21 @@ Object {
   ],
   ],
 }
 }
 `;
 `;
+
+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\\"}]}",
+      "subject": "test",
+      "version": "1",
+    },
+  },
+  "currentSchemaVersions": Array [],
+}
+`;

+ 44 - 0
kafka-ui-react-app/src/redux/reducers/schemas/__test__/fixtures.ts

@@ -52,3 +52,47 @@ export const schemaVersionsPayload: SchemaSubject[] = [
     compatibilityLevel: 'BACKWARD',
     compatibilityLevel: 'BACKWARD',
   },
   },
 ];
 ];
+
+export const newSchemaPayload: SchemaSubject = {
+  subject: 'test4',
+  version: '2',
+  id: 2,
+  schema:
+    '{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
+  compatibilityLevel: 'BACKWARD',
+};
+
+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',
+  },
+  {
+    subject: 'test3',
+    version: '1',
+    id: 5,
+    schema:
+      '{"type":"record","name":"MyRecord","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
+    compatibilityLevel: 'BACKWARD',
+  },
+  {
+    subject: 'test',
+    version: '2',
+    id: 2,
+    schema:
+      '{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
+    compatibilityLevel: 'BACKWARD',
+  },
+  {
+    subject: 'test4',
+    version: '2',
+    id: 2,
+    schema:
+      '{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
+    compatibilityLevel: 'BACKWARD',
+  },
+];

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

@@ -1,4 +1,5 @@
 import {
 import {
+  createSchemaAction,
   fetchSchemasByClusterNameAction,
   fetchSchemasByClusterNameAction,
   fetchSchemaVersionsAction,
   fetchSchemaVersionsAction,
 } from 'redux/actions';
 } from 'redux/actions';
@@ -17,6 +18,9 @@ describe('Schemas reducer', () => {
     expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
     expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
       initialState
       initialState
     );
     );
+    expect(reducer(undefined, createSchemaAction.request())).toEqual(
+      initialState
+    );
   });
   });
 
 
   it('reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload', () => {
   it('reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload', () => {
@@ -36,4 +40,10 @@ describe('Schemas reducer', () => {
       )
       )
     ).toMatchSnapshot();
     ).toMatchSnapshot();
   });
   });
+
+  it('reacts on POST_SCHEMA__SUCCESS and returns payload', () => {
+    expect(
+      reducer(undefined, createSchemaAction.success(schemaVersionsPayload[0]))
+    ).toMatchSnapshot();
+  });
 });
 });

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

@@ -1,10 +1,16 @@
 import {
 import {
+  createSchemaAction,
   fetchSchemasByClusterNameAction,
   fetchSchemasByClusterNameAction,
   fetchSchemaVersionsAction,
   fetchSchemaVersionsAction,
 } from 'redux/actions';
 } from 'redux/actions';
 import configureStore from 'redux/store/configureStore';
 import configureStore from 'redux/store/configureStore';
 import * as selectors from '../selectors';
 import * as selectors from '../selectors';
-import { clusterSchemasPayload, schemaVersionsPayload } from './fixtures';
+import {
+  clusterSchemasPayload,
+  clusterSchemasPayloadWithNewSchema,
+  newSchemaPayload,
+  schemaVersionsPayload,
+} from './fixtures';
 
 
 const store = configureStore();
 const store = configureStore();
 
 
@@ -13,6 +19,7 @@ describe('Schemas selectors', () => {
     it('returns fetch status', () => {
     it('returns fetch status', () => {
       expect(selectors.getIsSchemaListFetched(store.getState())).toBeFalsy();
       expect(selectors.getIsSchemaListFetched(store.getState())).toBeFalsy();
       expect(selectors.getIsSchemaVersionFetched(store.getState())).toBeFalsy();
       expect(selectors.getIsSchemaVersionFetched(store.getState())).toBeFalsy();
+      expect(selectors.getSchemaCreated(store.getState())).toBeFalsy();
     });
     });
 
 
     it('returns schema list', () => {
     it('returns schema list', () => {
@@ -34,6 +41,7 @@ describe('Schemas selectors', () => {
         fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
         fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
       );
       );
       store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload));
       store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload));
+      store.dispatch(createSchemaAction.success(newSchemaPayload));
     });
     });
 
 
     it('returns fetch status', () => {
     it('returns fetch status', () => {
@@ -41,11 +49,12 @@ describe('Schemas selectors', () => {
       expect(
       expect(
         selectors.getIsSchemaVersionFetched(store.getState())
         selectors.getIsSchemaVersionFetched(store.getState())
       ).toBeTruthy();
       ).toBeTruthy();
+      expect(selectors.getSchemaCreated(store.getState())).toBeTruthy();
     });
     });
 
 
     it('returns schema list', () => {
     it('returns schema list', () => {
       expect(selectors.getSchemaList(store.getState())).toEqual(
       expect(selectors.getSchemaList(store.getState())).toEqual(
-        clusterSchemasPayload
+        clusterSchemasPayloadWithNewSchema
       );
       );
     });
     });
 
 

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

@@ -32,12 +32,26 @@ const updateSchemaList = (
   }, initialMemo);
   }, initialMemo);
 };
 };
 
 
+const addToSchemaList = (
+  state: SchemasState,
+  payload: SchemaSubject
+): SchemasState => {
+  const newState: SchemasState = {
+    ...state,
+  };
+  newState.allNames.push(payload.subject as string);
+  newState.byName[payload.subject as string] = { ...payload };
+  return newState;
+};
+
 const reducer = (state = initialState, action: Action): SchemasState => {
 const reducer = (state = initialState, action: Action): SchemasState => {
   switch (action.type) {
   switch (action.type) {
     case 'GET_CLUSTER_SCHEMAS__SUCCESS':
     case 'GET_CLUSTER_SCHEMAS__SUCCESS':
       return updateSchemaList(state, action.payload);
       return updateSchemaList(state, action.payload);
     case 'GET_SCHEMA_VERSIONS__SUCCESS':
     case 'GET_SCHEMA_VERSIONS__SUCCESS':
       return { ...state, currentSchemaVersions: action.payload };
       return { ...state, currentSchemaVersions: action.payload };
+    case 'POST_SCHEMA__SUCCESS':
+      return addToSchemaList(state, action.payload);
     default:
     default:
       return state;
       return state;
   }
   }

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

@@ -15,16 +15,28 @@ const getSchemaVersionsFetchingStatus = createFetchingSelector(
   'GET_SCHEMA_VERSIONS'
   'GET_SCHEMA_VERSIONS'
 );
 );
 
 
+const getSchemaCreationStatus = createFetchingSelector('POST_SCHEMA');
+
 export const getIsSchemaListFetched = createSelector(
 export const getIsSchemaListFetched = createSelector(
   getSchemaListFetchingStatus,
   getSchemaListFetchingStatus,
   (status) => status === 'fetched'
   (status) => status === 'fetched'
 );
 );
 
 
+export const getIsSchemaListFetching = createSelector(
+  getSchemaListFetchingStatus,
+  (status) => status === 'fetching' || status === 'notFetched'
+);
+
 export const getIsSchemaVersionFetched = createSelector(
 export const getIsSchemaVersionFetched = createSelector(
   getSchemaVersionsFetchingStatus,
   getSchemaVersionsFetchingStatus,
   (status) => status === 'fetched'
   (status) => status === 'fetched'
 );
 );
 
 
+export const getSchemaCreated = createSelector(
+  getSchemaCreationStatus,
+  (status) => status === 'fetched'
+);
+
 export const getSchemaList = createSelector(
 export const getSchemaList = createSelector(
   getIsSchemaListFetched,
   getIsSchemaListFetched,
   getAllNames,
   getAllNames,