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"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
|
@ -29,9 +32,11 @@ const Details: React.FC<DetailsProps> = ({
|
|||
schema,
|
||||
clusterName,
|
||||
fetchSchemaVersions,
|
||||
fetchSchemasByClusterName,
|
||||
deleteSchema,
|
||||
versions,
|
||||
isFetched,
|
||||
areVersionsFetched,
|
||||
areSchemasFetched,
|
||||
}) => {
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const [
|
||||
|
@ -40,8 +45,9 @@ const Details: React.FC<DetailsProps> = ({
|
|||
] = 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<DetailsProps> = ({
|
|||
{subject}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
{areVersionsFetched && areSchemasFetched ? (
|
||||
<>
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
|
@ -78,19 +84,17 @@ const Details: React.FC<DetailsProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
<div className="level-right">
|
||||
<button
|
||||
className="button is-warning is-small level-item"
|
||||
<div className="level-right buttons">
|
||||
<Link
|
||||
className="button is-warning"
|
||||
type="button"
|
||||
title="in development"
|
||||
disabled
|
||||
to={clusterSchemaSchemaEditPath(clusterName, subject)}
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
</Link>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
className="button is-danger"
|
||||
type="button"
|
||||
title="in development"
|
||||
onClick={() => setDeleteSchemaConfirmationVisible(true)}
|
||||
>
|
||||
Remove
|
||||
|
|
|
@ -5,8 +5,13 @@ import {
|
|||
getIsSchemaVersionFetched,
|
||||
getSchema,
|
||||
getSortedSchemaVersions,
|
||||
getIsSchemaListFetched,
|
||||
} from 'redux/reducers/schemas/selectors';
|
||||
import { fetchSchemaVersions, deleteSchema } from 'redux/actions';
|
||||
import {
|
||||
fetchSchemaVersions,
|
||||
deleteSchema,
|
||||
fetchSchemasByClusterName,
|
||||
} from 'redux/actions';
|
||||
|
||||
import Details from './Details';
|
||||
|
||||
|
@ -28,12 +33,14 @@ const mapStateToProps = (
|
|||
subject,
|
||||
schema: getSchema(state, subject),
|
||||
versions: getSortedSchemaVersions(state),
|
||||
isFetched: getIsSchemaVersionFetched(state),
|
||||
areVersionsFetched: getIsSchemaVersionFetched(state),
|
||||
areSchemasFetched: getIsSchemaListFetched(state),
|
||||
clusterName,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchSchemaVersions,
|
||||
fetchSchemasByClusterName,
|
||||
deleteSchema,
|
||||
};
|
||||
|
||||
|
|
|
@ -42,7 +42,9 @@ describe('Details', () => {
|
|||
clusterName={clusterName}
|
||||
fetchSchemaVersions={fetchSchemaVersionsMock}
|
||||
deleteSchema={jest.fn()}
|
||||
isFetched
|
||||
fetchSchemasByClusterName={jest.fn()}
|
||||
areSchemasFetched
|
||||
areVersionsFetched
|
||||
versions={[]}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -71,14 +73,16 @@ describe('Details', () => {
|
|||
});
|
||||
|
||||
describe('when page with schema versions is loading', () => {
|
||||
const wrapper = shallow(setupWrapper({ isFetched: false }));
|
||||
const wrapper = shallow(setupWrapper({ areVersionsFetched: false }));
|
||||
|
||||
it('renders PageLoader', () => {
|
||||
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expect(confirmationModal.prop('isOpen')).toBeFalsy();
|
||||
wrapper.find('button').at(1).simulate('click');
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(findConfirmationModal().prop('isOpen')).toBeTruthy();
|
||||
// @ts-expect-error lack of typing of enzyme#invoke
|
||||
confirmationModal.invoke('onConfirm')();
|
||||
|
@ -138,7 +142,7 @@ describe('Details', () => {
|
|||
|
||||
it('calls deleteSchema after confirmation', () => {
|
||||
expect(confirmationModal.prop('isOpen')).toBeFalsy();
|
||||
wrapper.find('button').at(1).simulate('click');
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(findConfirmationModal().prop('isOpen')).toBeTruthy();
|
||||
// @ts-expect-error lack of typing of enzyme#invoke
|
||||
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
|
||||
className="level-right"
|
||||
className="level-right buttons"
|
||||
>
|
||||
<button
|
||||
className="button is-warning is-small level-item"
|
||||
disabled={true}
|
||||
title="in development"
|
||||
<Link
|
||||
className="button is-warning"
|
||||
to="/ui/clusters/testCluster/schemas/test/edit"
|
||||
type="button"
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
</Link>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
className="button is-danger"
|
||||
onClick={[Function]}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
|
@ -195,20 +193,18 @@ exports[`Details View when page with schema versions loaded when schema has vers
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="level-right"
|
||||
className="level-right buttons"
|
||||
>
|
||||
<button
|
||||
className="button is-warning is-small level-item"
|
||||
disabled={true}
|
||||
title="in development"
|
||||
<Link
|
||||
className="button is-warning"
|
||||
to="/ui/clusters/testCluster/schemas/test/edit"
|
||||
type="button"
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
</Link>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
className="button is-danger"
|
||||
onClick={[Function]}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
|
@ -344,20 +340,18 @@ exports[`Details View when page with schema versions loaded when versions are em
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="level-right"
|
||||
className="level-right buttons"
|
||||
>
|
||||
<button
|
||||
className="button is-warning is-small level-item"
|
||||
disabled={true}
|
||||
title="in development"
|
||||
<Link
|
||||
className="button is-warning"
|
||||
to="/ui/clusters/testCluster/schemas/test/edit"
|
||||
type="button"
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
</Link>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
className="button is-danger"
|
||||
onClick={[Function]}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
|
@ -416,3 +410,27 @@ exports[`Details View when page with schema versions loaded when versions are em
|
|||
</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 { 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<NewProps> = ({ createSchema }) => {
|
||||
const { clusterName } = useParams<{ clusterName: string }>();
|
||||
const history = useHistory();
|
||||
const {
|
||||
register,
|
||||
errors,
|
||||
|
@ -25,12 +26,17 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
|
|||
} = useForm<NewSchemaSubjectRaw>();
|
||||
|
||||
const onSubmit = React.useCallback(
|
||||
async ({ subject, schema }: NewSchemaSubjectRaw) => {
|
||||
async ({ subject, schema, schemaType }: NewSchemaSubjectRaw) => {
|
||||
try {
|
||||
await createSchema(clusterName, {
|
||||
subject,
|
||||
schema,
|
||||
schemaType: SchemaType.AVRO,
|
||||
schemaType,
|
||||
});
|
||||
history.push(clusterSchemaPath(clusterName, subject));
|
||||
} catch (e) {
|
||||
// Show Error
|
||||
}
|
||||
},
|
||||
[clusterName]
|
||||
);
|
||||
|
@ -62,7 +68,7 @@ const New: React.FC<NewProps> = ({ 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<NewProps> = ({ createSchema }) => {
|
|||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
ref={register}
|
||||
ref={register({
|
||||
required: 'Schema is required.',
|
||||
})}
|
||||
name="schema"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
@ -92,6 +100,26 @@ const New: React.FC<NewProps> = ({ createSchema }) => {
|
|||
<ErrorMessage errors={errors} name="schema" />
|
||||
</p>
|
||||
</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>
|
||||
<br />
|
||||
<div className="field">
|
||||
|
|
|
@ -151,6 +151,47 @@ exports[`New View matches snapshot 1`] = `
|
|||
/>
|
||||
</p>
|
||||
</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>
|
||||
<br />
|
||||
<div
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import ListContainer from './List/ListContainer';
|
||||
import DetailsContainer from './Details/DetailsContainer';
|
||||
import NewContainer from './New/NewContainer';
|
||||
import EditContainer from './Edit/EditContainer';
|
||||
|
||||
const Schemas: React.FC = () => (
|
||||
<Switch>
|
||||
|
@ -27,6 +28,11 @@ const Schemas: React.FC = () => (
|
|||
path={clusterSchemaPath(':clusterName', ':subject')}
|
||||
component={DetailsContainer}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/ui/clusters/:clusterName/schemas/:subject/edit"
|
||||
component={EditContainer}
|
||||
/>
|
||||
</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,
|
||||
subject: SchemaName
|
||||
) => `${clusterSchemasPath(clusterName)}/${subject}/latest`;
|
||||
export const clusterSchemaSchemaEditPath = (
|
||||
clusterName: ClusterName,
|
||||
subject: SchemaName
|
||||
) => `${clusterSchemasPath(clusterName)}/${subject}/edit`;
|
||||
|
||||
// Topics
|
||||
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 = {
|
||||
brokerCount: 1,
|
||||
|
@ -18,3 +24,13 @@ export const schemaPayload: NewSchemaSubject = {
|
|||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
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 thunks from 'redux/actions/thunks';
|
||||
import * as schemaFixtures from 'redux/reducers/schemas/__test__/fixtures';
|
||||
import {
|
||||
CompatibilityLevelCompatibilityEnum,
|
||||
SchemaType,
|
||||
} from 'generated-sources';
|
||||
import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
|
||||
import * as fixtures from 'redux/actions/__test__/fixtures';
|
||||
|
||||
|
@ -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', () => {
|
||||
it('fires DELETE_SCHEMA__SUCCESS on success', async () => {
|
||||
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'
|
||||
)<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(
|
||||
'DELETE_SCHEMA__REQUEST',
|
||||
'DELETE_SCHEMA__SUCCESS',
|
||||
|
|
|
@ -3,6 +3,8 @@ import {
|
|||
Configuration,
|
||||
NewSchemaSubject,
|
||||
SchemaSubject,
|
||||
CompatibilityLevelCompatibilityEnum,
|
||||
SchemaType,
|
||||
} from 'generated-sources';
|
||||
import {
|
||||
PromiseThunkResult,
|
||||
|
@ -13,6 +15,7 @@ import {
|
|||
import { BASE_PARAMS } from 'lib/constants';
|
||||
import * as actions from 'redux/actions';
|
||||
import { getResponse } from 'lib/errorHandling';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||
export const schemasApiClient = new SchemasApi(apiClientConf);
|
||||
|
@ -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 = (
|
||||
clusterName: ClusterName,
|
||||
subject: string
|
||||
|
|
Loading…
Add table
Reference in a new issue