From caec4eb1702c66bf443c0f8af917d83f7caedcbc Mon Sep 17 00:00:00 2001 From: Alexander Krivonosov <31561808+GneyHabub@users.noreply.github.com> Date: Thu, 22 Apr 2021 16:26:02 +0300 Subject: [PATCH] Schema registry views (#317) * Add schema type select to the form * Implement adding new version of a schema * Fix some issues & Increase test coverage * Add more tests * Add compatibility level update * Abstract updating schema into a separate thunk & test it * Remove warnings and skipped test * Update failed tests * Update failing tests * Update markup * Make the JSONEditor a part of the form * Fix linting problem * Fix errors --- kafka-ui-react-app/package-lock.json | 30 +- kafka-ui-react-app/package.json | 2 + .../components/Schemas/Details/Details.tsx | 30 +- .../Schemas/Details/DetailsContainer.ts | 11 +- .../Schemas/Details/__test__/Details.spec.tsx | 26 +- .../__snapshots__/Details.spec.tsx.snap | 66 ++-- .../src/components/Schemas/Edit/Edit.tsx | 172 ++++++++++ .../components/Schemas/Edit/EditContainer.ts | 38 +++ .../Schemas/Edit/__tests__/Edit.spec.tsx | 66 ++++ .../__snapshots__/Edit.spec.tsx.snap | 311 ++++++++++++++++++ .../src/components/Schemas/New/New.tsx | 48 ++- .../__test__/__snapshots__/New.spec.tsx.snap | 41 +++ .../src/components/Schemas/Schemas.tsx | 6 + .../common/JSONEditor/JSONEditor.tsx | 56 ++++ .../JSONEditor/__tests__/JSONEditor.spec.tsx | 10 + .../__snapshots__/JSONEditor.spec.tsx.snap | 43 +++ kafka-ui-react-app/src/lib/paths.ts | 4 + .../src/redux/actions/__test__/fixtures.ts | 18 +- .../actions/__test__/thunks/schemas.spec.ts | 79 +++++ .../src/redux/actions/actions.ts | 6 + .../src/redux/actions/thunks/schemas.ts | 60 ++++ 21 files changed, 1066 insertions(+), 57 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx create mode 100644 kafka-ui-react-app/src/components/Schemas/Edit/EditContainer.ts create mode 100644 kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx create mode 100644 kafka-ui-react-app/src/components/Schemas/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap create mode 100644 kafka-ui-react-app/src/components/common/JSONEditor/JSONEditor.tsx create mode 100644 kafka-ui-react-app/src/components/common/JSONEditor/__tests__/JSONEditor.spec.tsx create mode 100644 kafka-ui-react-app/src/components/common/JSONEditor/__tests__/__snapshots__/JSONEditor.spec.tsx.snap diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index 943e1d9145..b3eb1c294d 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/kafka-ui-react-app/package-lock.json @@ -3248,6 +3248,11 @@ "negotiator": "0.6.2" } }, + "ace-builds": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.12.tgz", + "integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg==" + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -6375,6 +6380,11 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -11979,11 +11989,15 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.isplainobject": { "version": "4.0.6", @@ -15208,6 +15222,18 @@ "object-assign": "^4.1.1" } }, + "react-ace": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-9.3.0.tgz", + "integrity": "sha512-RWPDwVobLvyD0wDoHHQqEnn9pNQBhMnmo6LmRACkaXxAg3UQZpse6x9JFLC5EXyWby+P3uolIlQPct4NFEBPNg==", + "requires": { + "ace-builds": "^1.4.6", + "diff-match-patch": "^1.0.4", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.7.2" + } + }, "react-app-polyfill": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz", diff --git a/kafka-ui-react-app/package.json b/kafka-ui-react-app/package.json index 14eca535e2..58ad6095c9 100644 --- a/kafka-ui-react-app/package.json +++ b/kafka-ui-react-app/package.json @@ -5,6 +5,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.3", "@hookform/error-message": "0.0.5", + "ace-builds": "^1.4.12", "@rooks/use-outside-click-ref": "^4.10.1", "bulma": "^0.9.2", "bulma-switch": "^2.0.0", @@ -16,6 +17,7 @@ "node-fetch": "^2.6.1", "pretty-ms": "^7.0.1", "react": "^17.0.1", + "react-ace": "^9.3.0", "react-datepicker": "^3.7.0", "react-dom": "^17.0.1", "react-hook-form": "^6.15.5", diff --git a/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx b/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx index d2ba74faf2..dbeda4843f 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/Details.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { useHistory } from 'react-router'; import { SchemaSubject } from 'generated-sources'; import { ClusterName, SchemaName } from 'redux/interfaces'; -import { clusterSchemasPath } from 'lib/paths'; +import { clusterSchemasPath, clusterSchemaSchemaEditPath } from 'lib/paths'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import ClusterContext from 'components/contexts/ClusterContext'; +import { Link } from 'react-router-dom'; import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; import PageLoader from 'components/common/PageLoader/PageLoader'; @@ -16,11 +17,13 @@ export interface DetailsProps { schema: SchemaSubject; clusterName: ClusterName; versions: SchemaSubject[]; - isFetched: boolean; + areVersionsFetched: boolean; + areSchemasFetched: boolean; fetchSchemaVersions: ( clusterName: ClusterName, schemaName: SchemaName ) => void; + fetchSchemasByClusterName: (clusterName: ClusterName) => void; deleteSchema: (clusterName: ClusterName, subject: string) => Promise; } @@ -29,9 +32,11 @@ const Details: React.FC = ({ schema, clusterName, fetchSchemaVersions, + fetchSchemasByClusterName, deleteSchema, versions, - isFetched, + areVersionsFetched, + areSchemasFetched, }) => { const { isReadOnly } = React.useContext(ClusterContext); const [ @@ -40,8 +45,9 @@ const Details: React.FC = ({ ] = React.useState(false); React.useEffect(() => { + fetchSchemasByClusterName(clusterName); fetchSchemaVersions(clusterName, subject); - }, [fetchSchemaVersions, clusterName]); + }, [fetchSchemaVersions, fetchSchemasByClusterName, clusterName]); const history = useHistory(); const onDelete = React.useCallback(() => { @@ -63,7 +69,7 @@ const Details: React.FC = ({ {subject} - {isFetched ? ( + {areVersionsFetched && areSchemasFetched ? ( <>
@@ -78,19 +84,17 @@ const Details: React.FC = ({
{!isReadOnly && ( -
- +
- +
- +
- +
`; + +exports[`Details View when page with schemas are loading matches snapshot 1`] = ` +
+
+ + test + +
+ +
+`; diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx new file mode 100644 index 0000000000..d99f2aa2eb --- /dev/null +++ b/kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx @@ -0,0 +1,172 @@ +import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; +import { + CompatibilityLevelCompatibilityEnum, + SchemaSubject, + SchemaType, +} from 'generated-sources'; +import { clusterSchemaPath, clusterSchemasPath } from 'lib/paths'; +import React from 'react'; +import { ClusterName, NewSchemaSubjectRaw, SchemaName } from 'redux/interfaces'; +import PageLoader from 'components/common/PageLoader/PageLoader'; +import { useHistory } from 'react-router'; +import JSONEditor from 'components/common/JSONEditor/JSONEditor'; +import { useForm } from 'react-hook-form'; + +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; +} + +const Edit = ({ + subject, + schema, + clusterName, + schemasAreFetched, + fetchSchemasByClusterName, + updateSchema, +}: EditProps) => { + React.useEffect(() => { + if (!schemasAreFetched) fetchSchemasByClusterName(clusterName); + }, [clusterName, fetchSchemasByClusterName]); + + const { + register, + handleSubmit, + formState: { isSubmitting }, + control, + } = useForm({ mode: 'onChange' }); + + const getFormattedSchema = React.useCallback( + () => 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; + }) => { + await updateSchema( + schema, + newSchema, + schemaType, + compatibilityLevel, + clusterName, + subject + ); + history.push(clusterSchemaPath(clusterName, subject)); + }, + [schema, register, control, clusterName, subject, updateSchema, history] + ); + + return ( +
+
+
+ + Edit + +
+
+ + {schemasAreFetched && !isSubmitting ? ( +
+
+
+
Schema Type
+
+ +
+
+ +
+
Compatibility Level
+
+ +
+
+
+
+

Latest Schema

+ +
+
+

New Schema

+ +
+
+ +
+
+ ) : ( + + )} +
+ ); +}; + +export default Edit; diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/EditContainer.ts b/kafka-ui-react-app/src/components/Schemas/Edit/EditContainer.ts new file mode 100644 index 0000000000..f01908594d --- /dev/null +++ b/kafka-ui-react-app/src/components/Schemas/Edit/EditContainer.ts @@ -0,0 +1,38 @@ +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; + +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)); diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx new file mode 100644 index 0000000000..ad9205c6f0 --- /dev/null +++ b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/Edit.spec.tsx @@ -0,0 +1,66 @@ +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'; + +describe('Edit Component', () => { + const mockSchema = { + subject: 'Subject', + version: '1', + id: 1, + schema: '{"schema": "schema"}', + compatibilityLevel: 'BACKWARD', + schemaType: SchemaType.AVRO, + }; + + const setupWrapper = (props: Partial = {}) => ( + + ); + + 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( + + {setupWrapper({ + schemasAreFetched: false, + fetchSchemasByClusterName: mockFetch, + })} + + ); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + }); + + describe('when schemas are fetched', () => { + const component = shallow(setupWrapper()); + it('matches the snapshot', () => { + expect(component).toMatchSnapshot(); + }); + it('shows editor', () => { + expect(component.find('JSONEditor').length).toEqual(2); + expect(component.find('button').exists()).toBeTruthy(); + }); + it('does not fetch them', () => { + const mockFetch = jest.fn(); + shallow(setupWrapper()); + expect(mockFetch).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap new file mode 100644 index 0000000000..290e3612ae --- /dev/null +++ b/kafka-ui-react-app/src/components/Schemas/Edit/__tests__/__snapshots__/Edit.spec.tsx.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Edit Component when schemas are fetched matches the snapshot 1`] = ` +
+
+
+ + Edit + +
+
+
+
+
+
+ Schema Type +
+
+ +
+
+
+
+ Compatibility Level +
+
+ +
+
+
+
+

+ Latest Schema +

+ +
+
+

+ New Schema +

+ +
+
+ +
+
+
+`; + +exports[`Edit Component when schemas are not fetched matches the snapshot 1`] = ` +
+
+
+ + Edit + +
+
+ +
+`; diff --git a/kafka-ui-react-app/src/components/Schemas/New/New.tsx b/kafka-ui-react-app/src/components/Schemas/New/New.tsx index f18cdf1638..8ad306852d 100644 --- a/kafka-ui-react-app/src/components/Schemas/New/New.tsx +++ b/kafka-ui-react-app/src/components/Schemas/New/New.tsx @@ -3,10 +3,10 @@ import { ClusterName, NewSchemaSubjectRaw } from 'redux/interfaces'; import { useForm } from 'react-hook-form'; import { ErrorMessage } from '@hookform/error-message'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; -import { clusterSchemasPath } from 'lib/paths'; +import { clusterSchemaPath, clusterSchemasPath } from 'lib/paths'; import { NewSchemaSubject, SchemaType } from 'generated-sources'; import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants'; -import { useParams } from 'react-router'; +import { useHistory, useParams } from 'react-router'; export interface NewProps { createSchema: ( @@ -17,6 +17,7 @@ export interface NewProps { const New: React.FC = ({ createSchema }) => { const { clusterName } = useParams<{ clusterName: string }>(); + const history = useHistory(); const { register, errors, @@ -25,12 +26,17 @@ const New: React.FC = ({ createSchema }) => { } = useForm(); const onSubmit = React.useCallback( - async ({ subject, schema }: NewSchemaSubjectRaw) => { - await createSchema(clusterName, { - subject, - schema, - schemaType: SchemaType.AVRO, - }); + async ({ subject, schema, schemaType }: NewSchemaSubjectRaw) => { + try { + await createSchema(clusterName, { + subject, + schema, + schemaType, + }); + history.push(clusterSchemaPath(clusterName, subject)); + } catch (e) { + // Show Error + } }, [clusterName] ); @@ -62,7 +68,7 @@ const New: React.FC = ({ createSchema }) => { className="input" placeholder="Schema Name" ref={register({ - required: 'Topic Name is required.', + required: 'Schema Name is required.', pattern: { value: SCHEMA_NAME_VALIDATION_PATTERN, message: 'Only alphanumeric, _, -, and . allowed', @@ -83,7 +89,9 @@ const New: React.FC = ({ createSchema }) => {