Browse Source

Schema show page (#196)

* Latest version added

* Changes from [PR-186] applied

* Schema Versions get method added

* Schema show page created

* Updated JSONViewer

* Details component refactored

* Tests for Messages component updated

* Details component and packages updated

* Feedback

* Delete testSchema

Co-authored-by: Oleg Shuralev <workshur@gmail.com>
Guzel738 4 years ago
parent
commit
44949b9af2
23 changed files with 358 additions and 81 deletions
  1. 2 0
      docker/kafka-ui.yaml
  2. 3 3
      kafka-ui-react-app/package-lock.json
  3. 4 2
      kafka-ui-react-app/package.json
  4. 110 0
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  5. 39 0
      kafka-ui-react-app/src/components/Schemas/Details/DetailsContainer.ts
  6. 43 0
      kafka-ui-react-app/src/components/Schemas/Details/LatestVersionItem.tsx
  7. 23 0
      kafka-ui-react-app/src/components/Schemas/Details/SchemaVersion.tsx
  8. 5 3
      kafka-ui-react-app/src/components/Schemas/List/List.tsx
  9. 20 3
      kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx
  10. 6 0
      kafka-ui-react-app/src/components/Schemas/Schemas.tsx
  11. 3 3
      kafka-ui-react-app/src/components/Schemas/SchemasContainer.tsx
  12. 2 23
      kafka-ui-react-app/src/components/Topics/Details/Messages/MessageItem.tsx
  13. 1 1
      kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesTable.tsx
  14. 2 2
      kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/MessageItem.spec.tsx
  15. 1 23
      kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap
  16. 15 0
      kafka-ui-react-app/src/components/common/JSONViewer/JSONViewer.tsx
  17. 20 0
      kafka-ui-react-app/src/components/common/JSONViewer/themes/google.ts
  18. 6 0
      kafka-ui-react-app/src/redux/actions/actions.ts
  19. 19 10
      kafka-ui-react-app/src/redux/actions/thunks.ts
  20. 5 2
      kafka-ui-react-app/src/redux/interfaces/schema.ts
  21. 3 0
      kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts
  22. 25 6
      kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts
  23. 1 0
      kafka-ui-react-app/src/theme/bulma_overrides.scss

+ 2 - 0
docker/kafka-ui.yaml

@@ -30,6 +30,8 @@ services:
     environment:
       ZOOKEEPER_CLIENT_PORT: 2181
       ZOOKEEPER_TICK_TIME: 2000
+    ports:
+      - 2181:2181
 
   kafka0:
     image: confluentinc/cp-kafka:5.1.0

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

@@ -4962,9 +4962,9 @@
       "dev": true
     },
     "bulma": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.2.tgz",
-      "integrity": "sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA=="
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.2.tgz",
+      "integrity": "sha512-e14EF+3VSZ488yL/lJH0tR8mFWiEQVCMi/BQUMi2TGMBOk+zrDg4wryuwm/+dRSHJw0gMawp2tsW7X1JYUCE3A=="
     },
     "bulma-switch": {
       "version": "2.0.0",

+ 4 - 2
kafka-ui-react-app/package.json

@@ -3,7 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "bulma": "^0.8.2",
+    "bulma": "^0.9.2",
     "bulma-switch": "^2.0.0",
     "classnames": "^2.2.6",
     "date-fns": "^2.16.1",
@@ -112,6 +112,8 @@
   },
   "proxy": "http://localhost:8080",
   "jest": {
-    "snapshotSerializers": ["enzyme-to-json/serializer"]
+    "snapshotSerializers": [
+      "enzyme-to-json/serializer"
+    ]
   }
 }

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

@@ -0,0 +1,110 @@
+import React from 'react';
+import { SchemaSubject } from 'generated-sources';
+import { ClusterName, SchemaName } from 'redux/interfaces';
+import { clusterSchemasPath } from 'lib/paths';
+import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
+import SchemaVersion from './SchemaVersion';
+import LatestVersionItem from './LatestVersionItem';
+import PageLoader from '../../common/PageLoader/PageLoader';
+
+interface DetailsProps {
+  schema: SchemaSubject;
+  clusterName: ClusterName;
+  versions: SchemaSubject[];
+  isFetched: boolean;
+  fetchSchemaVersions: (
+    clusterName: ClusterName,
+    schemaName: SchemaName
+  ) => void;
+}
+
+const Details: React.FC<DetailsProps> = ({
+  schema,
+  clusterName,
+  fetchSchemaVersions,
+  versions,
+  isFetched,
+}) => {
+  React.useEffect(() => {
+    fetchSchemaVersions(clusterName, schema.subject as SchemaName);
+  }, [fetchSchemaVersions, clusterName]);
+  return (
+    <div className="section">
+      <div className="level">
+        <Breadcrumb
+          links={[
+            {
+              href: clusterSchemasPath(clusterName),
+              label: 'Schema Registry',
+            },
+          ]}
+        >
+          {schema.subject}
+        </Breadcrumb>
+      </div>
+      <div className="box">
+        <div className="level">
+          <div className="level-left">
+            <div className="level-item">
+              <div className="mr-1">
+                <b>Latest Version</b>
+              </div>
+              <div className="tag is-info is-light" title="Version">
+                #{schema.version}
+              </div>
+            </div>
+          </div>
+          <div className="level-right">
+            <button
+              className="button is-primary is-small level-item"
+              type="button"
+              title="in development"
+              disabled
+            >
+              Create Schema
+            </button>
+            <button
+              className="button is-warning is-small level-item"
+              type="button"
+              title="in development"
+              disabled
+            >
+              Update Schema
+            </button>
+            <button
+              className="button is-danger is-small level-item"
+              type="button"
+              title="in development"
+              disabled
+            >
+              Delete
+            </button>
+          </div>
+        </div>
+        <LatestVersionItem schema={schema} />
+      </div>
+      {isFetched ? (
+        <div className="box">
+          <table className="table is-striped is-fullwidth">
+            <thead>
+              <tr>
+                <th>Version</th>
+                <th>ID</th>
+                <th>Schema</th>
+              </tr>
+            </thead>
+            <tbody>
+              {versions.map((version) => (
+                <SchemaVersion key={version.id} version={version} />
+              ))}
+            </tbody>
+          </table>
+        </div>
+      ) : (
+        <PageLoader />
+      )}
+    </div>
+  );
+};
+
+export default Details;

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

@@ -0,0 +1,39 @@
+import { connect } from 'react-redux';
+import { ClusterName, RootState } from 'redux/interfaces';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import {
+  getIsSchemaVersionFetched,
+  getSchema,
+  getSortedSchemaVersions,
+} from 'redux/reducers/schemas/selectors';
+import { fetchSchemaVersions } 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
+) => ({
+  schema: getSchema(state, subject),
+  versions: getSortedSchemaVersions(state),
+  isFetched: getIsSchemaVersionFetched(state),
+  clusterName,
+});
+
+const mapDispatchToProps = {
+  fetchSchemaVersions,
+};
+
+export default withRouter(
+  connect(mapStateToProps, mapDispatchToProps)(Details)
+);

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

@@ -0,0 +1,43 @@
+import React from 'react';
+import { SchemaSubject } from 'generated-sources';
+import JSONViewer from 'components/common/JSONViewer/JSONViewer';
+
+interface LatestVersionProps {
+  schema: SchemaSubject;
+}
+
+const LatestVersionItem: React.FC<LatestVersionProps> = ({
+  schema: { id, subject, schema, compatibilityLevel },
+}) => {
+  return (
+    <div className="tile is-ancestor mt-1">
+      <div className="tile is-4 is-parent">
+        <div className="tile is-child">
+          <table className="table is-fullwidth">
+            <tbody>
+              <tr>
+                <td>ID</td>
+                <td>{id}</td>
+              </tr>
+              <tr>
+                <td>Subject</td>
+                <td>{subject}</td>
+              </tr>
+              <tr>
+                <td>Compatibility</td>
+                <td>{compatibilityLevel}</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+      <div className="tile is-parent">
+        <div className="tile is-child box py-1">
+          <JSONViewer data={JSON.parse(schema as string)} />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default LatestVersionItem;

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

@@ -0,0 +1,23 @@
+import React from 'react';
+import { SchemaSubject } from 'generated-sources';
+import JSONViewer from 'components/common/JSONViewer/JSONViewer';
+
+interface SchemaVersionProps {
+  version: SchemaSubject;
+}
+
+const SchemaVersion: React.FC<SchemaVersionProps> = ({
+  version: { version, id, schema },
+}) => {
+  return (
+    <tr>
+      <td>{version}</td>
+      <td>{id}</td>
+      <td className="py-0">
+        <JSONViewer data={JSON.parse(schema as string)} />
+      </td>
+    </tr>
+  );
+};
+
+export default SchemaVersion;

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

@@ -1,5 +1,5 @@
-import { SchemaSubject } from 'generated-sources';
 import React from 'react';
+import { SchemaSubject } from 'generated-sources';
 import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
 import ListItem from './ListItem';
 
@@ -16,11 +16,13 @@ const List: React.FC<ListProps> = ({ schemas }) => {
           <thead>
             <tr>
               <th>Schema Name</th>
+              <th>Version</th>
+              <th>Compatibility</th>
             </tr>
           </thead>
           <tbody>
-            {schemas.map(({ subject }) => (
-              <ListItem subject={subject} />
+            {schemas.map((subject) => (
+              <ListItem key={subject.id} subject={subject} />
             ))}
           </tbody>
         </table>

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

@@ -1,13 +1,30 @@
 import React from 'react';
+import { SchemaSubject } from 'generated-sources';
+import { NavLink } from 'react-router-dom';
 
 interface ListItemProps {
-  subject?: string;
+  subject: SchemaSubject;
 }
 
-const ListItem: React.FC<ListItemProps> = ({ subject }) => {
+const ListItem: React.FC<ListItemProps> = ({
+  subject: { subject, version, compatibilityLevel },
+}) => {
   return (
     <tr>
-      <td>{subject}</td>
+      <td>
+        <NavLink
+          exact
+          to={`schemas/${subject}/latest`}
+          activeClassName="is-active"
+          className="title is-6"
+        >
+          {subject}
+        </NavLink>
+      </td>
+      <td>{version}</td>
+      <td>
+        <span className="tag is-link">{compatibilityLevel}</span>
+      </td>
     </tr>
   );
 };

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

@@ -3,6 +3,7 @@ import { ClusterName } from 'redux/interfaces';
 import { Switch, Route, useParams } from 'react-router-dom';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import ListContainer from './List/ListContainer';
+import DetailsContainer from './Details/DetailsContainer';
 
 interface SchemasProps {
   isFetched: boolean;
@@ -31,6 +32,11 @@ const Schemas: React.FC<SchemasProps> = ({
           path="/ui/clusters/:clusterName/schemas"
           component={ListContainer}
         />
+        <Route
+          exact
+          path="/ui/clusters/:clusterName/schemas/:subject/latest"
+          component={DetailsContainer}
+        />
       </Switch>
     );
   }

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

@@ -1,8 +1,8 @@
 import { connect } from 'react-redux';
-import { RootState } from '../../redux/interfaces';
-import { fetchSchemasByClusterName } from '../../redux/actions';
+import { RootState } from 'redux/interfaces';
+import { fetchSchemasByClusterName } from 'redux/actions';
+import { getIsSchemaListFetched } from 'redux/reducers/schemas/selectors';
 import Schemas from './Schemas';
-import { getIsSchemaListFetched } from '../../redux/reducers/schemas/selectors';
 
 const mapStateToProps = (state: RootState) => ({
   isFetched: getIsSchemaListFetched(state),

+ 2 - 23
kafka-ui-react-app/src/components/Topics/Details/Messages/MessageItem.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { format } from 'date-fns';
-import JSONTree from 'react-json-tree';
 import { TopicMessage } from 'generated-sources';
+import JSONViewer from 'components/common/JSONViewer/JSONViewer';
 
 export interface MessageItemProp {
   partition: TopicMessage['partition'];
@@ -21,28 +21,7 @@ const MessageItem: React.FC<MessageItemProp> = ({
     <td style={{ width: 150 }}>{offset}</td>
     <td style={{ width: 100 }}>{partition}</td>
     <td style={{ wordBreak: 'break-word' }}>
-      {content && (
-        <JSONTree
-          data={content}
-          hideRoot
-          invertTheme={false}
-          theme={{
-            tree: ({ style }) => ({
-              style: {
-                ...style,
-                backgroundColor: undefined,
-                marginLeft: 0,
-                marginTop: 0,
-              },
-            }),
-            value: ({ style }) => ({
-              style: { ...style, marginLeft: 0 },
-            }),
-            base0D: '#3273dc',
-            base0B: '#363636',
-          }}
-        />
-      )}
+      {content && <JSONViewer data={content as { [key: string]: string }} />}
     </td>
   </tr>
 );

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Details/Messages/MessagesTable.tsx

@@ -32,7 +32,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
                 partition={partition}
                 offset={offset}
                 timestamp={timestamp}
-                content={content as Record<string, unknown>}
+                content={content as { [key: string]: string }}
               />
             )
           )}

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

@@ -14,7 +14,7 @@ describe('MessageItem', () => {
 
       expect(wrapper.find('tr').length).toEqual(1);
       expect(wrapper.find('td').length).toEqual(4);
-      expect(wrapper.find('JSONTree').length).toEqual(1);
+      expect(wrapper.find('JSONViewer').length).toEqual(1);
     });
 
     it('matches snapshot', () => {
@@ -28,7 +28,7 @@ describe('MessageItem', () => {
 
       expect(wrapper.find('tr').length).toEqual(1);
       expect(wrapper.find('td').length).toEqual(4);
-      expect(wrapper.find('JSONTree').length).toEqual(0);
+      expect(wrapper.find('JSONViewer').length).toEqual(0);
     });
 
     it('matches snapshot', () => {

+ 1 - 23
kafka-ui-react-app/src/components/Topics/Details/Messages/__test__/__snapshots__/MessageItem.spec.tsx.snap

@@ -36,35 +36,13 @@ exports[`MessageItem when content is defined matches snapshot 1`] = `
       }
     }
   >
-    <JSONTree
-      collectionLimit={50}
+    <JSONViewer
       data={
         Object {
           "foo": "bar",
           "key": "val",
         }
       }
-      getItemString={[Function]}
-      hideRoot={true}
-      invertTheme={false}
-      isCustomNode={[Function]}
-      keyPath={
-        Array [
-          "root",
-        ]
-      }
-      labelRenderer={[Function]}
-      postprocessValue={[Function]}
-      shouldExpandNode={[Function]}
-      theme={
-        Object {
-          "base0B": "#363636",
-          "base0D": "#3273dc",
-          "tree": [Function],
-          "value": [Function],
-        }
-      }
-      valueRenderer={[Function]}
     />
   </td>
 </tr>

+ 15 - 0
kafka-ui-react-app/src/components/common/JSONViewer/JSONViewer.tsx

@@ -0,0 +1,15 @@
+import React from 'react';
+import JSONTree from 'react-json-tree';
+import theme from './themes/google';
+
+interface JSONViewerProps {
+  data: {
+    [key: string]: string;
+  };
+}
+
+const JSONViewer: React.FC<JSONViewerProps> = ({ data }) => (
+  <JSONTree data={data} theme={theme} shouldExpandNode={() => true} hideRoot />
+);
+
+export default JSONViewer;

+ 20 - 0
kafka-ui-react-app/src/components/common/JSONViewer/themes/google.ts

@@ -0,0 +1,20 @@
+export default {
+  scheme: 'google',
+  author: 'seth wright (http://sethawright.com)',
+  base00: '#1d1f21',
+  base01: '#282a2e',
+  base02: '#373b41',
+  base03: '#969896',
+  base04: '#b4b7b4',
+  base05: '#c5c8c6',
+  base06: '#e0e0e0',
+  base07: '#ffffff',
+  base08: '#CC342B',
+  base09: '#F96A38',
+  base0A: '#FBA922',
+  base0B: '#198844',
+  base0C: '#3971ED',
+  base0D: '#3971ED',
+  base0E: '#A36AC7',
+  base0F: '#3971ED',
+};

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

@@ -103,3 +103,9 @@ export const fetchSchemasByClusterNameAction = createAsyncAction(
   'GET_CLUSTER_SCHEMAS__SUCCESS',
   'GET_CLUSTER_SCHEMAS__FAILURE'
 )<undefined, SchemaSubject[], undefined>();
+
+export const fetchSchemaVersionsAction = createAsyncAction(
+  'GET_SCHEMA_VERSIONS__REQUEST',
+  'GET_SCHEMA_VERSIONS__SUCCESS',
+  'GET_SCHEMA_VERSIONS__FAILURE'
+)<undefined, SchemaSubject[], undefined>();

+ 19 - 10
kafka-ui-react-app/src/redux/actions/thunks.ts

@@ -5,7 +5,6 @@ import {
   Topic,
   TopicFormData,
   TopicConfig,
-  SchemaSubject,
 } from 'generated-sources';
 import {
   ConsumerGroupID,
@@ -16,6 +15,7 @@ import {
   TopicMessageQueryParams,
   TopicFormFormattedParams,
   TopicFormDataRaw,
+  SchemaName,
 } from 'redux/interfaces';
 
 import { BASE_PARAMS } from 'lib/constants';
@@ -257,17 +257,26 @@ export const fetchSchemasByClusterName = (
 ): PromiseThunk<void> => async (dispatch) => {
   dispatch(actions.fetchSchemasByClusterNameAction.request());
   try {
-    const schemaNames = await apiClient.getSchemas({ clusterName });
-
-    // TODO: Remove me after API refactoring
-    const schemas: SchemaSubject[] = await Promise.all(
-      schemaNames.map((schemaName) =>
-        apiClient.getLatestSchema({ clusterName, schemaName })
-      )
-    );
-
+    const schemas = await apiClient.getSchemas({ clusterName });
     dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
   } catch (e) {
     dispatch(actions.fetchSchemasByClusterNameAction.failure());
   }
 };
+
+export const fetchSchemaVersions = (
+  clusterName: ClusterName,
+  subject: SchemaName
+): PromiseThunk<void> => async (dispatch) => {
+  if (!subject) return;
+  dispatch(actions.fetchSchemaVersionsAction.request());
+  try {
+    const versions = await apiClient.getAllVersionsBySubject({
+      clusterName,
+      subject,
+    });
+    dispatch(actions.fetchSchemaVersionsAction.success(versions));
+  } catch (e) {
+    dispatch(actions.fetchSchemaVersionsAction.failure());
+  }
+};

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

@@ -1,6 +1,9 @@
 import { SchemaSubject } from 'generated-sources';
 
+export type SchemaName = string;
+
 export interface SchemasState {
-  byName: { [name: string]: SchemaSubject };
-  allNames: string[];
+  byName: { [subject: string]: SchemaSubject };
+  allNames: SchemaName[];
+  currentSchemaVersions: SchemaSubject[];
 }

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

@@ -4,6 +4,7 @@ import { Action, SchemasState } from 'redux/interfaces';
 export const initialState: SchemasState = {
   byName: {},
   allNames: [],
+  currentSchemaVersions: [],
 };
 
 const updateSchemaList = (
@@ -35,6 +36,8 @@ 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 };
     default:
       return state;
   }

+ 25 - 6
kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts

@@ -11,19 +11,38 @@ const getSchemaListFetchingStatus = createFetchingSelector(
   'GET_CLUSTER_SCHEMAS'
 );
 
+const getSchemaVersionsFetchingStatus = createFetchingSelector(
+  'GET_SCHEMA_VERSIONS'
+);
+
 export const getIsSchemaListFetched = createSelector(
   getSchemaListFetchingStatus,
   (status) => status === 'fetched'
 );
 
+export const getIsSchemaVersionFetched = createSelector(
+  getSchemaVersionsFetchingStatus,
+  (status) => status === 'fetched'
+);
+
 export const getSchemaList = createSelector(
   getIsSchemaListFetched,
   getAllNames,
   getSchemaMap,
-  (isFetched, allNames, byName) => {
-    if (!isFetched) {
-      return [];
-    }
-    return allNames.map((subject) => byName[subject]);
-  }
+  (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 }) =>
+    currentSchemaVersions.sort((a, b) => a.id - b.id)
 );

+ 1 - 0
kafka-ui-react-app/src/theme/bulma_overrides.scss

@@ -2,6 +2,7 @@
 @import "../../node_modules/bulma/sass/base/_all.sass";
 @import "../../node_modules/bulma/sass/elements/_all.sass";
 @import "../../node_modules/bulma/sass/form/_all.sass";
+@import "../../node_modules/bulma/sass/helpers/_all.sass";
 @import "../../node_modules/bulma/sass/components/_all.sass";
 @import "../../node_modules/bulma/sass/grid/_all.sass";
 @import "../../node_modules/bulma/sass/layout/_all.sass";