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
This commit is contained in:
parent
083e3f7de0
commit
caec4eb170
21 changed files with 1066 additions and 57 deletions
30
kafka-ui-react-app/package-lock.json
generated
30
kafka-ui-react-app/package-lock.json
generated
|
@ -3248,6 +3248,11 @@
|
||||||
"negotiator": "0.6.2"
|
"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": {
|
"acorn": {
|
||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
|
@ -6375,6 +6380,11 @@
|
||||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
"dev": true
|
"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": {
|
"diff-sequences": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
|
||||||
|
@ -11979,11 +11989,15 @@
|
||||||
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
|
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
|
||||||
"dev": true
|
"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": {
|
"lodash.isequal": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
|
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.isplainobject": {
|
"lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
|
@ -15208,6 +15222,18 @@
|
||||||
"object-assign": "^4.1.1"
|
"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": {
|
"react-app-polyfill": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||||
"@hookform/error-message": "0.0.5",
|
"@hookform/error-message": "0.0.5",
|
||||||
|
"ace-builds": "^1.4.12",
|
||||||
"@rooks/use-outside-click-ref": "^4.10.1",
|
"@rooks/use-outside-click-ref": "^4.10.1",
|
||||||
"bulma": "^0.9.2",
|
"bulma": "^0.9.2",
|
||||||
"bulma-switch": "^2.0.0",
|
"bulma-switch": "^2.0.0",
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"pretty-ms": "^7.0.1",
|
"pretty-ms": "^7.0.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
|
"react-ace": "^9.3.0",
|
||||||
"react-datepicker": "^3.7.0",
|
"react-datepicker": "^3.7.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-hook-form": "^6.15.5",
|
"react-hook-form": "^6.15.5",
|
||||||
|
|
|
@ -2,9 +2,10 @@ import React from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { SchemaSubject } from 'generated-sources';
|
import { SchemaSubject } from 'generated-sources';
|
||||||
import { ClusterName, SchemaName } from 'redux/interfaces';
|
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 Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||||
import ClusterContext from 'components/contexts/ClusterContext';
|
import ClusterContext from 'components/contexts/ClusterContext';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
|
||||||
|
@ -16,11 +17,13 @@ export interface DetailsProps {
|
||||||
schema: SchemaSubject;
|
schema: SchemaSubject;
|
||||||
clusterName: ClusterName;
|
clusterName: ClusterName;
|
||||||
versions: SchemaSubject[];
|
versions: SchemaSubject[];
|
||||||
isFetched: boolean;
|
areVersionsFetched: boolean;
|
||||||
|
areSchemasFetched: boolean;
|
||||||
fetchSchemaVersions: (
|
fetchSchemaVersions: (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
schemaName: SchemaName
|
schemaName: SchemaName
|
||||||
) => void;
|
) => void;
|
||||||
|
fetchSchemasByClusterName: (clusterName: ClusterName) => void;
|
||||||
deleteSchema: (clusterName: ClusterName, subject: string) => Promise<void>;
|
deleteSchema: (clusterName: ClusterName, subject: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,9 +32,11 @@ const Details: React.FC<DetailsProps> = ({
|
||||||
schema,
|
schema,
|
||||||
clusterName,
|
clusterName,
|
||||||
fetchSchemaVersions,
|
fetchSchemaVersions,
|
||||||
|
fetchSchemasByClusterName,
|
||||||
deleteSchema,
|
deleteSchema,
|
||||||
versions,
|
versions,
|
||||||
isFetched,
|
areVersionsFetched,
|
||||||
|
areSchemasFetched,
|
||||||
}) => {
|
}) => {
|
||||||
const { isReadOnly } = React.useContext(ClusterContext);
|
const { isReadOnly } = React.useContext(ClusterContext);
|
||||||
const [
|
const [
|
||||||
|
@ -40,8 +45,9 @@ const Details: React.FC<DetailsProps> = ({
|
||||||
] = React.useState(false);
|
] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
fetchSchemasByClusterName(clusterName);
|
||||||
fetchSchemaVersions(clusterName, subject);
|
fetchSchemaVersions(clusterName, subject);
|
||||||
}, [fetchSchemaVersions, clusterName]);
|
}, [fetchSchemaVersions, fetchSchemasByClusterName, clusterName]);
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const onDelete = React.useCallback(() => {
|
const onDelete = React.useCallback(() => {
|
||||||
|
@ -63,7 +69,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||||
{subject}
|
{subject}
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
{isFetched ? (
|
{areVersionsFetched && areSchemasFetched ? (
|
||||||
<>
|
<>
|
||||||
<div className="box">
|
<div className="box">
|
||||||
<div className="level">
|
<div className="level">
|
||||||
|
@ -78,19 +84,17 @@ const Details: React.FC<DetailsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<div className="level-right">
|
<div className="level-right buttons">
|
||||||
<button
|
<Link
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning"
|
||||||
type="button"
|
type="button"
|
||||||
title="in development"
|
to={clusterSchemaSchemaEditPath(clusterName, subject)}
|
||||||
disabled
|
|
||||||
>
|
>
|
||||||
Update Schema
|
Update Schema
|
||||||
</button>
|
</Link>
|
||||||
<button
|
<button
|
||||||
className="button is-danger is-small level-item"
|
className="button is-danger"
|
||||||
type="button"
|
type="button"
|
||||||
title="in development"
|
|
||||||
onClick={() => setDeleteSchemaConfirmationVisible(true)}
|
onClick={() => setDeleteSchemaConfirmationVisible(true)}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
|
|
@ -5,8 +5,13 @@ import {
|
||||||
getIsSchemaVersionFetched,
|
getIsSchemaVersionFetched,
|
||||||
getSchema,
|
getSchema,
|
||||||
getSortedSchemaVersions,
|
getSortedSchemaVersions,
|
||||||
|
getIsSchemaListFetched,
|
||||||
} from 'redux/reducers/schemas/selectors';
|
} from 'redux/reducers/schemas/selectors';
|
||||||
import { fetchSchemaVersions, deleteSchema } from 'redux/actions';
|
import {
|
||||||
|
fetchSchemaVersions,
|
||||||
|
deleteSchema,
|
||||||
|
fetchSchemasByClusterName,
|
||||||
|
} from 'redux/actions';
|
||||||
|
|
||||||
import Details from './Details';
|
import Details from './Details';
|
||||||
|
|
||||||
|
@ -28,12 +33,14 @@ const mapStateToProps = (
|
||||||
subject,
|
subject,
|
||||||
schema: getSchema(state, subject),
|
schema: getSchema(state, subject),
|
||||||
versions: getSortedSchemaVersions(state),
|
versions: getSortedSchemaVersions(state),
|
||||||
isFetched: getIsSchemaVersionFetched(state),
|
areVersionsFetched: getIsSchemaVersionFetched(state),
|
||||||
|
areSchemasFetched: getIsSchemaListFetched(state),
|
||||||
clusterName,
|
clusterName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchSchemaVersions,
|
fetchSchemaVersions,
|
||||||
|
fetchSchemasByClusterName,
|
||||||
deleteSchema,
|
deleteSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,9 @@ describe('Details', () => {
|
||||||
clusterName={clusterName}
|
clusterName={clusterName}
|
||||||
fetchSchemaVersions={fetchSchemaVersionsMock}
|
fetchSchemaVersions={fetchSchemaVersionsMock}
|
||||||
deleteSchema={jest.fn()}
|
deleteSchema={jest.fn()}
|
||||||
isFetched
|
fetchSchemasByClusterName={jest.fn()}
|
||||||
|
areSchemasFetched
|
||||||
|
areVersionsFetched
|
||||||
versions={[]}
|
versions={[]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -71,14 +73,16 @@ describe('Details', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when page with schema versions is loading', () => {
|
describe('when page with schema versions is loading', () => {
|
||||||
const wrapper = shallow(setupWrapper({ isFetched: false }));
|
const wrapper = shallow(setupWrapper({ areVersionsFetched: false }));
|
||||||
|
|
||||||
it('renders PageLoader', () => {
|
it('renders PageLoader', () => {
|
||||||
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
|
expect(
|
||||||
|
shallow(setupWrapper({ areVersionsFetched: false }))
|
||||||
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -129,7 +133,7 @@ describe('Details', () => {
|
||||||
|
|
||||||
it('calls deleteSchema after confirmation', () => {
|
it('calls deleteSchema after confirmation', () => {
|
||||||
expect(confirmationModal.prop('isOpen')).toBeFalsy();
|
expect(confirmationModal.prop('isOpen')).toBeFalsy();
|
||||||
wrapper.find('button').at(1).simulate('click');
|
wrapper.find('button').simulate('click');
|
||||||
expect(findConfirmationModal().prop('isOpen')).toBeTruthy();
|
expect(findConfirmationModal().prop('isOpen')).toBeTruthy();
|
||||||
// @ts-expect-error lack of typing of enzyme#invoke
|
// @ts-expect-error lack of typing of enzyme#invoke
|
||||||
confirmationModal.invoke('onConfirm')();
|
confirmationModal.invoke('onConfirm')();
|
||||||
|
@ -138,7 +142,7 @@ describe('Details', () => {
|
||||||
|
|
||||||
it('calls deleteSchema after confirmation', () => {
|
it('calls deleteSchema after confirmation', () => {
|
||||||
expect(confirmationModal.prop('isOpen')).toBeFalsy();
|
expect(confirmationModal.prop('isOpen')).toBeFalsy();
|
||||||
wrapper.find('button').at(1).simulate('click');
|
wrapper.find('button').simulate('click');
|
||||||
expect(findConfirmationModal().prop('isOpen')).toBeTruthy();
|
expect(findConfirmationModal().prop('isOpen')).toBeTruthy();
|
||||||
// @ts-expect-error lack of typing of enzyme#invoke
|
// @ts-expect-error lack of typing of enzyme#invoke
|
||||||
wrapper.find('mock-ConfirmationModal').invoke('onCancel')();
|
wrapper.find('mock-ConfirmationModal').invoke('onCancel')();
|
||||||
|
@ -167,5 +171,17 @@ describe('Details', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when page with schemas are loading', () => {
|
||||||
|
const wrapper = shallow(setupWrapper({ areSchemasFetched: false }));
|
||||||
|
|
||||||
|
it('renders PageLoader', () => {
|
||||||
|
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches snapshot', () => {
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -49,20 +49,18 @@ exports[`Details View Initial state matches snapshot 1`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="level-right"
|
className="level-right buttons"
|
||||||
>
|
>
|
||||||
<button
|
<Link
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning"
|
||||||
disabled={true}
|
to="/ui/clusters/testCluster/schemas/test/edit"
|
||||||
title="in development"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Update Schema
|
Update Schema
|
||||||
</button>
|
</Link>
|
||||||
<button
|
<button
|
||||||
className="button is-danger is-small level-item"
|
className="button is-danger"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
title="in development"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
@ -195,20 +193,18 @@ exports[`Details View when page with schema versions loaded when schema has vers
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="level-right"
|
className="level-right buttons"
|
||||||
>
|
>
|
||||||
<button
|
<Link
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning"
|
||||||
disabled={true}
|
to="/ui/clusters/testCluster/schemas/test/edit"
|
||||||
title="in development"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Update Schema
|
Update Schema
|
||||||
</button>
|
</Link>
|
||||||
<button
|
<button
|
||||||
className="button is-danger is-small level-item"
|
className="button is-danger"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
title="in development"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
@ -344,20 +340,18 @@ exports[`Details View when page with schema versions loaded when versions are em
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="level-right"
|
className="level-right buttons"
|
||||||
>
|
>
|
||||||
<button
|
<Link
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning"
|
||||||
disabled={true}
|
to="/ui/clusters/testCluster/schemas/test/edit"
|
||||||
title="in development"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Update Schema
|
Update Schema
|
||||||
</button>
|
</Link>
|
||||||
<button
|
<button
|
||||||
className="button is-danger is-small level-item"
|
className="button is-danger"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
title="in development"
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
@ -416,3 +410,27 @@ exports[`Details View when page with schema versions loaded when versions are em
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Details View when page with schemas are loading matches snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="section"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="level"
|
||||||
|
>
|
||||||
|
<Breadcrumb
|
||||||
|
links={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"href": "/ui/clusters/testCluster/schemas",
|
||||||
|
"label": "Schema Registry",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
test
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
<PageLoader />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
172
kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx
Normal file
172
kafka-ui-react-app/src/components/Schemas/Edit/Edit.tsx
Normal file
|
@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Edit = ({
|
||||||
|
subject,
|
||||||
|
schema,
|
||||||
|
clusterName,
|
||||||
|
schemasAreFetched,
|
||||||
|
fetchSchemasByClusterName,
|
||||||
|
updateSchema,
|
||||||
|
}: EditProps) => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!schemasAreFetched) fetchSchemasByClusterName(clusterName);
|
||||||
|
}, [clusterName, fetchSchemasByClusterName]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
control,
|
||||||
|
} = useForm<NewSchemaSubjectRaw>({ 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 (
|
||||||
|
<div className="section">
|
||||||
|
<div className="level">
|
||||||
|
<div className="level-item level-left">
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
href: clusterSchemasPath(clusterName),
|
||||||
|
label: 'Schema Registry',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: clusterSchemaPath(clusterName, subject),
|
||||||
|
label: subject,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{schemasAreFetched && !isSubmitting ? (
|
||||||
|
<div className="box">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="mt-3 is-flex-direction-column"
|
||||||
|
>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h5 className="title is-5 mb-2">Schema Type</h5>
|
||||||
|
<div className="select">
|
||||||
|
<select
|
||||||
|
name="schemaType"
|
||||||
|
ref={register({
|
||||||
|
required: 'Schema Type is required.',
|
||||||
|
})}
|
||||||
|
defaultValue={schema.schemaType}
|
||||||
|
>
|
||||||
|
{Object.keys(SchemaType).map((type: string) => (
|
||||||
|
<option key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h5 className="title is-5 mb-2">Compatibility Level</h5>
|
||||||
|
<div className="select">
|
||||||
|
<select
|
||||||
|
name="compatibilityLevel"
|
||||||
|
ref={register()}
|
||||||
|
defaultValue={schema.compatibilityLevel}
|
||||||
|
>
|
||||||
|
{Object.keys(CompatibilityLevelCompatibilityEnum).map(
|
||||||
|
(level: string) => (
|
||||||
|
<option key={level} value={level}>
|
||||||
|
{level}
|
||||||
|
</option>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-one-half">
|
||||||
|
<h4 className="title is-5 mb-2">Latest Schema</h4>
|
||||||
|
<JSONEditor
|
||||||
|
readonly
|
||||||
|
value={getFormattedSchema()}
|
||||||
|
name="latestSchema"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column is-one-half">
|
||||||
|
<h4 className="title is-5 mb-2">New Schema</h4>
|
||||||
|
<JSONEditor
|
||||||
|
control={control}
|
||||||
|
value={getFormattedSchema()}
|
||||||
|
name="newSchema"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="button is-primary">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<PageLoader />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Edit;
|
|
@ -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<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));
|
|
@ -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<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(
|
||||||
|
<StaticRouter>
|
||||||
|
{setupWrapper({
|
||||||
|
schemasAreFetched: false,
|
||||||
|
fetchSchemasByClusterName: mockFetch,
|
||||||
|
})}
|
||||||
|
</StaticRouter>
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,311 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Edit Component when schemas are fetched matches the snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="section"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="level"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="level-item level-left"
|
||||||
|
>
|
||||||
|
<Breadcrumb
|
||||||
|
links={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"href": "/ui/clusters/ClusterName/schemas",
|
||||||
|
"label": "Schema Registry",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"href": "/ui/clusters/ClusterName/schemas/Subject/latest",
|
||||||
|
"label": "Subject",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="box"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
className="mt-3 is-flex-direction-column"
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
className="title is-5 mb-2"
|
||||||
|
>
|
||||||
|
Schema Type
|
||||||
|
</h5>
|
||||||
|
<div
|
||||||
|
className="select"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
defaultValue="AVRO"
|
||||||
|
name="schemaType"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
key="AVRO"
|
||||||
|
value="AVRO"
|
||||||
|
>
|
||||||
|
AVRO
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key="JSON"
|
||||||
|
value="JSON"
|
||||||
|
>
|
||||||
|
JSON
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key="PROTOBUF"
|
||||||
|
value="PROTOBUF"
|
||||||
|
>
|
||||||
|
PROTOBUF
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
className="title is-5 mb-2"
|
||||||
|
>
|
||||||
|
Compatibility Level
|
||||||
|
</h5>
|
||||||
|
<div
|
||||||
|
className="select"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
defaultValue="BACKWARD"
|
||||||
|
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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="columns"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="column is-one-half"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
className="title is-5 mb-2"
|
||||||
|
>
|
||||||
|
Latest Schema
|
||||||
|
</h4>
|
||||||
|
<JSONEditor
|
||||||
|
name="latestSchema"
|
||||||
|
readonly={true}
|
||||||
|
value="{
|
||||||
|
\\"schema\\": \\"schema\\"
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="column is-one-half"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
className="title is-5 mb-2"
|
||||||
|
>
|
||||||
|
New Schema
|
||||||
|
</h4>
|
||||||
|
<JSONEditor
|
||||||
|
control={
|
||||||
|
Object {
|
||||||
|
"defaultValuesRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"fieldArrayDefaultValuesRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"fieldArrayNamesRef": Object {
|
||||||
|
"current": Set {},
|
||||||
|
},
|
||||||
|
"fieldArrayValuesRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"fieldsRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"fieldsWithValidationRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"formState": Object {
|
||||||
|
"dirtyFields": Object {},
|
||||||
|
"errors": Object {},
|
||||||
|
"isDirty": false,
|
||||||
|
"isSubmitSuccessful": false,
|
||||||
|
"isSubmitted": false,
|
||||||
|
"isSubmitting": false,
|
||||||
|
"isValid": true,
|
||||||
|
"isValidating": false,
|
||||||
|
"submitCount": 0,
|
||||||
|
"touched": Object {},
|
||||||
|
},
|
||||||
|
"formStateRef": Object {
|
||||||
|
"current": Object {
|
||||||
|
"dirtyFields": Object {},
|
||||||
|
"errors": Object {},
|
||||||
|
"isDirty": false,
|
||||||
|
"isSubmitSuccessful": false,
|
||||||
|
"isSubmitted": false,
|
||||||
|
"isSubmitting": false,
|
||||||
|
"isValid": true,
|
||||||
|
"isValidating": false,
|
||||||
|
"submitCount": 0,
|
||||||
|
"touched": Object {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"getValues": [Function],
|
||||||
|
"isFormDirty": [Function],
|
||||||
|
"mode": Object {
|
||||||
|
"isOnAll": false,
|
||||||
|
"isOnBlur": false,
|
||||||
|
"isOnChange": true,
|
||||||
|
"isOnSubmit": false,
|
||||||
|
"isOnTouch": false,
|
||||||
|
},
|
||||||
|
"reValidateMode": Object {
|
||||||
|
"isReValidateOnBlur": false,
|
||||||
|
"isReValidateOnChange": true,
|
||||||
|
},
|
||||||
|
"readFormStateRef": Object {
|
||||||
|
"current": Object {
|
||||||
|
"constructor": true,
|
||||||
|
"dirtyFields": true,
|
||||||
|
"errors": true,
|
||||||
|
"isDirty": true,
|
||||||
|
"isSubmitSuccessful": true,
|
||||||
|
"isSubmitted": true,
|
||||||
|
"isSubmitting": true,
|
||||||
|
"isValid": true,
|
||||||
|
"isValidating": true,
|
||||||
|
"submitCount": true,
|
||||||
|
"touched": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"register": [Function],
|
||||||
|
"removeFieldEventListener": [Function],
|
||||||
|
"resetFieldArrayFunctionRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"setValue": [Function],
|
||||||
|
"shallowFieldsStateRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"shouldUnregister": true,
|
||||||
|
"trigger": [Function],
|
||||||
|
"unregister": [Function],
|
||||||
|
"updateFormState": [Function],
|
||||||
|
"updateWatchedValue": [Function],
|
||||||
|
"useWatchFieldsRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"useWatchRenderFunctionsRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"validFieldsRef": Object {
|
||||||
|
"current": Object {},
|
||||||
|
},
|
||||||
|
"validateResolver": undefined,
|
||||||
|
"watchInternal": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name="newSchema"
|
||||||
|
value="{
|
||||||
|
\\"schema\\": \\"schema\\"
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Edit Component when schemas are not fetched matches the snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="section"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="level"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="level-item level-left"
|
||||||
|
>
|
||||||
|
<Breadcrumb
|
||||||
|
links={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"href": "/ui/clusters/ClusterName/schemas",
|
||||||
|
"label": "Schema Registry",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"href": "/ui/clusters/ClusterName/schemas/Subject/latest",
|
||||||
|
"label": "Subject",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Breadcrumb>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PageLoader />
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -3,10 +3,10 @@ import { ClusterName, NewSchemaSubjectRaw } from 'redux/interfaces';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { ErrorMessage } from '@hookform/error-message';
|
import { ErrorMessage } from '@hookform/error-message';
|
||||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
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 { NewSchemaSubject, SchemaType } from 'generated-sources';
|
||||||
import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants';
|
import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants';
|
||||||
import { useParams } from 'react-router';
|
import { useHistory, useParams } from 'react-router';
|
||||||
|
|
||||||
export interface NewProps {
|
export interface NewProps {
|
||||||
createSchema: (
|
createSchema: (
|
||||||
|
@ -17,6 +17,7 @@ export interface NewProps {
|
||||||
|
|
||||||
const New: React.FC<NewProps> = ({ createSchema }) => {
|
const New: React.FC<NewProps> = ({ createSchema }) => {
|
||||||
const { clusterName } = useParams<{ clusterName: string }>();
|
const { clusterName } = useParams<{ clusterName: string }>();
|
||||||
|
const history = useHistory();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
errors,
|
errors,
|
||||||
|
@ -25,12 +26,17 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
|
||||||
} = useForm<NewSchemaSubjectRaw>();
|
} = useForm<NewSchemaSubjectRaw>();
|
||||||
|
|
||||||
const onSubmit = React.useCallback(
|
const onSubmit = React.useCallback(
|
||||||
async ({ subject, schema }: NewSchemaSubjectRaw) => {
|
async ({ subject, schema, schemaType }: NewSchemaSubjectRaw) => {
|
||||||
await createSchema(clusterName, {
|
try {
|
||||||
subject,
|
await createSchema(clusterName, {
|
||||||
schema,
|
subject,
|
||||||
schemaType: SchemaType.AVRO,
|
schema,
|
||||||
});
|
schemaType,
|
||||||
|
});
|
||||||
|
history.push(clusterSchemaPath(clusterName, subject));
|
||||||
|
} catch (e) {
|
||||||
|
// Show Error
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[clusterName]
|
[clusterName]
|
||||||
);
|
);
|
||||||
|
@ -62,7 +68,7 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
|
||||||
className="input"
|
className="input"
|
||||||
placeholder="Schema Name"
|
placeholder="Schema Name"
|
||||||
ref={register({
|
ref={register({
|
||||||
required: 'Topic Name is required.',
|
required: 'Schema Name is required.',
|
||||||
pattern: {
|
pattern: {
|
||||||
value: SCHEMA_NAME_VALIDATION_PATTERN,
|
value: SCHEMA_NAME_VALIDATION_PATTERN,
|
||||||
message: 'Only alphanumeric, _, -, and . allowed',
|
message: 'Only alphanumeric, _, -, and . allowed',
|
||||||
|
@ -83,7 +89,9 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<textarea
|
<textarea
|
||||||
className="textarea"
|
className="textarea"
|
||||||
ref={register}
|
ref={register({
|
||||||
|
required: 'Schema is required.',
|
||||||
|
})}
|
||||||
name="schema"
|
name="schema"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
|
@ -92,6 +100,26 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
|
||||||
<ErrorMessage errors={errors} name="schema" />
|
<ErrorMessage errors={errors} name="schema" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Schema Type *</label>
|
||||||
|
<div className="control select">
|
||||||
|
<select
|
||||||
|
ref={register({
|
||||||
|
required: 'Schema Type is required.',
|
||||||
|
})}
|
||||||
|
name="schemaType"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
<option value={SchemaType.AVRO}>AVRO</option>
|
||||||
|
<option value={SchemaType.JSON}>JSON</option>
|
||||||
|
<option value={SchemaType.PROTOBUF}>PROTOBUF</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p className="help is-danger">
|
||||||
|
<ErrorMessage errors={errors} name="schemaType" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div className="field">
|
<div className="field">
|
||||||
|
|
|
@ -151,6 +151,47 @@ exports[`New View matches snapshot 1`] = `
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="label"
|
||||||
|
>
|
||||||
|
Schema Type *
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="control select"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
disabled={false}
|
||||||
|
name="schemaType"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="AVRO"
|
||||||
|
>
|
||||||
|
AVRO
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="JSON"
|
||||||
|
>
|
||||||
|
JSON
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="PROTOBUF"
|
||||||
|
>
|
||||||
|
PROTOBUF
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="help is-danger"
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
errors={Object {}}
|
||||||
|
name="schemaType"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
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';
|
import NewContainer from './New/NewContainer';
|
||||||
|
import EditContainer from './Edit/EditContainer';
|
||||||
|
|
||||||
const Schemas: React.FC = () => (
|
const Schemas: React.FC = () => (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -27,6 +28,11 @@ const Schemas: React.FC = () => (
|
||||||
path={clusterSchemaPath(':clusterName', ':subject')}
|
path={clusterSchemaPath(':clusterName', ':subject')}
|
||||||
component={DetailsContainer}
|
component={DetailsContainer}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/ui/clusters/:clusterName/schemas/:subject/edit"
|
||||||
|
component={EditContainer}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import AceEditor from 'react-ace';
|
||||||
|
import 'ace-builds/src-noconflict/mode-json5';
|
||||||
|
import 'ace-builds/src-noconflict/theme-dawn';
|
||||||
|
import React from 'react';
|
||||||
|
import { Control, Controller } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface JSONEditorProps {
|
||||||
|
readonly?: boolean;
|
||||||
|
onChange?: (e: string) => void;
|
||||||
|
value: string;
|
||||||
|
name: string;
|
||||||
|
control?: Control;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JSONEditor: React.FC<JSONEditorProps> = ({
|
||||||
|
readonly,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
}) => {
|
||||||
|
if (control) {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
as={
|
||||||
|
<AceEditor
|
||||||
|
defaultValue={value}
|
||||||
|
mode="json5"
|
||||||
|
theme="dawn"
|
||||||
|
name={name}
|
||||||
|
tabSize={2}
|
||||||
|
width="100%"
|
||||||
|
wrapEnabled
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AceEditor
|
||||||
|
mode="json5"
|
||||||
|
theme="dawn"
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
tabSize={2}
|
||||||
|
width="100%"
|
||||||
|
readOnly={readonly}
|
||||||
|
onChange={onChange}
|
||||||
|
wrapEnabled
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JSONEditor;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
|
||||||
|
|
||||||
|
describe('JSONEditor component', () => {
|
||||||
|
it('matches the snapshot', () => {
|
||||||
|
const component = shallow(<JSONEditor value="{}" name="name" />);
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`JSONEditor component matches the snapshot 1`] = `
|
||||||
|
<ReactAce
|
||||||
|
cursorStart={1}
|
||||||
|
editorProps={Object {}}
|
||||||
|
enableBasicAutocompletion={false}
|
||||||
|
enableLiveAutocompletion={false}
|
||||||
|
enableSnippets={false}
|
||||||
|
focus={false}
|
||||||
|
fontSize={12}
|
||||||
|
height="500px"
|
||||||
|
highlightActiveLine={true}
|
||||||
|
maxLines={null}
|
||||||
|
minLines={null}
|
||||||
|
mode="json5"
|
||||||
|
name="name"
|
||||||
|
navigateToFileEnd={true}
|
||||||
|
onChange={null}
|
||||||
|
onLoad={null}
|
||||||
|
onPaste={null}
|
||||||
|
onScroll={null}
|
||||||
|
placeholder={null}
|
||||||
|
readOnly={false}
|
||||||
|
scrollMargin={
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
setOptions={Object {}}
|
||||||
|
showGutter={true}
|
||||||
|
showPrintMargin={true}
|
||||||
|
style={Object {}}
|
||||||
|
tabSize={2}
|
||||||
|
theme="dawn"
|
||||||
|
value="{}"
|
||||||
|
width="100%"
|
||||||
|
wrapEnabled={true}
|
||||||
|
/>
|
||||||
|
`;
|
|
@ -24,6 +24,10 @@ export const clusterSchemaPath = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
subject: SchemaName
|
subject: SchemaName
|
||||||
) => `${clusterSchemasPath(clusterName)}/${subject}/latest`;
|
) => `${clusterSchemasPath(clusterName)}/${subject}/latest`;
|
||||||
|
export const clusterSchemaSchemaEditPath = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
subject: SchemaName
|
||||||
|
) => `${clusterSchemasPath(clusterName)}/${subject}/edit`;
|
||||||
|
|
||||||
// Topics
|
// Topics
|
||||||
export const clusterTopicsPath = (clusterName: ClusterName) =>
|
export const clusterTopicsPath = (clusterName: ClusterName) =>
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { ClusterStats, NewSchemaSubject, SchemaType } from 'generated-sources';
|
import {
|
||||||
|
ClusterStats,
|
||||||
|
CompatibilityLevelCompatibilityEnum,
|
||||||
|
NewSchemaSubject,
|
||||||
|
SchemaSubject,
|
||||||
|
SchemaType,
|
||||||
|
} from 'generated-sources';
|
||||||
|
|
||||||
export const clusterStats: ClusterStats = {
|
export const clusterStats: ClusterStats = {
|
||||||
brokerCount: 1,
|
brokerCount: 1,
|
||||||
|
@ -18,3 +24,13 @@ export const schemaPayload: NewSchemaSubject = {
|
||||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||||
schemaType: SchemaType.JSON,
|
schemaType: SchemaType.JSON,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const schema: SchemaSubject = {
|
||||||
|
subject: 'NewSchema',
|
||||||
|
schema:
|
||||||
|
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||||
|
schemaType: SchemaType.JSON,
|
||||||
|
version: '1',
|
||||||
|
id: 1,
|
||||||
|
compatibilityLevel: CompatibilityLevelCompatibilityEnum.BACKWARD,
|
||||||
|
};
|
||||||
|
|
|
@ -2,6 +2,10 @@ import fetchMock from 'fetch-mock-jest';
|
||||||
import * as actions from 'redux/actions/actions';
|
import * as actions from 'redux/actions/actions';
|
||||||
import * as thunks from 'redux/actions/thunks';
|
import * as thunks from 'redux/actions/thunks';
|
||||||
import * as schemaFixtures from 'redux/reducers/schemas/__test__/fixtures';
|
import * as schemaFixtures from 'redux/reducers/schemas/__test__/fixtures';
|
||||||
|
import {
|
||||||
|
CompatibilityLevelCompatibilityEnum,
|
||||||
|
SchemaType,
|
||||||
|
} from 'generated-sources';
|
||||||
import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
|
import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
|
||||||
import * as fixtures from 'redux/actions/__test__/fixtures';
|
import * as fixtures from 'redux/actions/__test__/fixtures';
|
||||||
|
|
||||||
|
@ -124,6 +128,47 @@ describe('Thunks', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateSchemaCompatibilityLevel', () => {
|
||||||
|
it('creates UPDATE_SCHEMA__SUCCESS when patching a schema', async () => {
|
||||||
|
fetchMock.putOnce(
|
||||||
|
`/api/clusters/${clusterName}/schemas/${subject}/compatibility`,
|
||||||
|
200
|
||||||
|
);
|
||||||
|
await store.dispatch(
|
||||||
|
thunks.updateSchemaCompatibilityLevel(
|
||||||
|
clusterName,
|
||||||
|
subject,
|
||||||
|
CompatibilityLevelCompatibilityEnum.BACKWARD
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(store.getActions()).toEqual([
|
||||||
|
actions.updateSchemaCompatibilityLevelAction.request(),
|
||||||
|
actions.updateSchemaCompatibilityLevelAction.success(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates UPDATE_SCHEMA__SUCCESS when failing to patch a schema', async () => {
|
||||||
|
fetchMock.putOnce(
|
||||||
|
`/api/clusters/${clusterName}/schemas/${subject}/compatibility`,
|
||||||
|
404
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await store.dispatch(
|
||||||
|
thunks.updateSchemaCompatibilityLevel(
|
||||||
|
clusterName,
|
||||||
|
subject,
|
||||||
|
CompatibilityLevelCompatibilityEnum.BACKWARD
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.status).toEqual(404);
|
||||||
|
expect(store.getActions()).toEqual([
|
||||||
|
actions.updateSchemaCompatibilityLevelAction.request(),
|
||||||
|
actions.updateSchemaCompatibilityLevelAction.failure({}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('deleteSchema', () => {
|
describe('deleteSchema', () => {
|
||||||
it('fires DELETE_SCHEMA__SUCCESS on success', async () => {
|
it('fires DELETE_SCHEMA__SUCCESS on success', async () => {
|
||||||
fetchMock.deleteOnce(
|
fetchMock.deleteOnce(
|
||||||
|
@ -156,4 +201,38 @@ describe('Thunks', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateSchema', () => {
|
||||||
|
it('calls createSchema', () => {
|
||||||
|
store.dispatch(
|
||||||
|
thunks.updateSchema(
|
||||||
|
fixtures.schema,
|
||||||
|
fixtures.schemaPayload.schema,
|
||||||
|
SchemaType.AVRO,
|
||||||
|
CompatibilityLevelCompatibilityEnum.BACKWARD,
|
||||||
|
clusterName,
|
||||||
|
subject
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(store.getActions()).toEqual([
|
||||||
|
actions.createSchemaAction.request(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls updateSchema and does not call createSchema when schema does not change', () => {
|
||||||
|
store.dispatch(
|
||||||
|
thunks.updateSchema(
|
||||||
|
fixtures.schema,
|
||||||
|
fixtures.schema.schema,
|
||||||
|
SchemaType.JSON,
|
||||||
|
CompatibilityLevelCompatibilityEnum.FORWARD,
|
||||||
|
clusterName,
|
||||||
|
subject
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(store.getActions()).toEqual([
|
||||||
|
actions.updateSchemaCompatibilityLevelAction.request(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -130,6 +130,12 @@ export const createSchemaAction = createAsyncAction(
|
||||||
'POST_SCHEMA__FAILURE'
|
'POST_SCHEMA__FAILURE'
|
||||||
)<undefined, SchemaSubject, { alert?: FailurePayload }>();
|
)<undefined, SchemaSubject, { alert?: FailurePayload }>();
|
||||||
|
|
||||||
|
export const updateSchemaCompatibilityLevelAction = createAsyncAction(
|
||||||
|
'PATCH_SCHEMA_COMPATIBILITY__REQUEST',
|
||||||
|
'PATCH_SCHEMA_COMPATIBILITY__SUCCESS',
|
||||||
|
'PATCH_SCHEMA_COMPATIBILITY__FAILURE'
|
||||||
|
)<undefined, undefined, { alert?: FailurePayload }>();
|
||||||
|
|
||||||
export const deleteSchemaAction = createAsyncAction(
|
export const deleteSchemaAction = createAsyncAction(
|
||||||
'DELETE_SCHEMA__REQUEST',
|
'DELETE_SCHEMA__REQUEST',
|
||||||
'DELETE_SCHEMA__SUCCESS',
|
'DELETE_SCHEMA__SUCCESS',
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {
|
||||||
Configuration,
|
Configuration,
|
||||||
NewSchemaSubject,
|
NewSchemaSubject,
|
||||||
SchemaSubject,
|
SchemaSubject,
|
||||||
|
CompatibilityLevelCompatibilityEnum,
|
||||||
|
SchemaType,
|
||||||
} from 'generated-sources';
|
} from 'generated-sources';
|
||||||
import {
|
import {
|
||||||
PromiseThunkResult,
|
PromiseThunkResult,
|
||||||
|
@ -13,6 +15,7 @@ import {
|
||||||
import { BASE_PARAMS } from 'lib/constants';
|
import { BASE_PARAMS } from 'lib/constants';
|
||||||
import * as actions from 'redux/actions';
|
import * as actions from 'redux/actions';
|
||||||
import { getResponse } from 'lib/errorHandling';
|
import { getResponse } from 'lib/errorHandling';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
const apiClientConf = new Configuration(BASE_PARAMS);
|
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||||
export const schemasApiClient = new SchemasApi(apiClientConf);
|
export const schemasApiClient = new SchemasApi(apiClientConf);
|
||||||
|
@ -68,6 +71,63 @@ export const createSchema = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateSchemaCompatibilityLevel = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
subject: string,
|
||||||
|
compatibilityLevel: CompatibilityLevelCompatibilityEnum
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
dispatch(actions.updateSchemaCompatibilityLevelAction.request());
|
||||||
|
try {
|
||||||
|
await schemasApiClient.updateSchemaCompatibilityLevel({
|
||||||
|
clusterName,
|
||||||
|
subject,
|
||||||
|
compatibilityLevel: {
|
||||||
|
compatibility: compatibilityLevel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch(actions.updateSchemaCompatibilityLevelAction.success());
|
||||||
|
} catch (error) {
|
||||||
|
const response = await getResponse(error);
|
||||||
|
const alert: FailurePayload = {
|
||||||
|
subject: 'compatibilityLevel',
|
||||||
|
title: `Compatibility level ${subject}`,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
dispatch(actions.updateSchemaCompatibilityLevelAction.failure({ alert }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSchema = (
|
||||||
|
latestSchema: SchemaSubject,
|
||||||
|
newSchema: string,
|
||||||
|
newSchemaType: SchemaType,
|
||||||
|
newCompatibilityLevel: CompatibilityLevelCompatibilityEnum,
|
||||||
|
clusterName: string,
|
||||||
|
subject: string
|
||||||
|
): PromiseThunkResult => async (dispatch) => {
|
||||||
|
if (
|
||||||
|
(newSchema &&
|
||||||
|
!isEqual(JSON.parse(latestSchema.schema), JSON.parse(newSchema))) ||
|
||||||
|
newSchemaType !== latestSchema.schemaType
|
||||||
|
) {
|
||||||
|
await dispatch(
|
||||||
|
createSchema(clusterName, {
|
||||||
|
...latestSchema,
|
||||||
|
schema: newSchema || latestSchema.schema,
|
||||||
|
schemaType: newSchemaType || latestSchema.schemaType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (newCompatibilityLevel !== latestSchema.compatibilityLevel) {
|
||||||
|
await dispatch(
|
||||||
|
updateSchemaCompatibilityLevel(
|
||||||
|
clusterName,
|
||||||
|
subject,
|
||||||
|
newCompatibilityLevel
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
export const deleteSchema = (
|
export const deleteSchema = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
subject: string
|
subject: string
|
||||||
|
|
Loading…
Add table
Reference in a new issue