Fix the problem with instant redirect (#390)

* Fix the problem with instant redirect

* Rewrite updateSchema thunk
This commit is contained in:
Alexander Krivonosov 2021-05-01 09:08:32 +03:00 committed by GitHub
parent f3c0866940
commit 42a1c97686
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 112 additions and 139 deletions

View file

@ -33,6 +33,7 @@ const LatestVersionItem: React.FC<LatestVersionProps> = ({
<div className="tile is-parent"> <div className="tile is-parent">
<div className="tile is-child box"> <div className="tile is-child box">
<JSONEditor <JSONEditor
isFixedHeight
name="schema" name="schema"
value={JSON.stringify(JSON.parse(schema), null, '\t')} value={JSON.stringify(JSON.parse(schema), null, '\t')}
showGutter={false} showGutter={false}

View file

@ -15,6 +15,7 @@ const SchemaVersion: React.FC<SchemaVersionProps> = ({
<td>{id}</td> <td>{id}</td>
<td> <td>
<JSONEditor <JSONEditor
isFixedHeight
name="schema" name="schema"
value={JSON.stringify(JSON.parse(schema), null, '\t')} value={JSON.stringify(JSON.parse(schema), null, '\t')}
showGutter={false} showGutter={false}

View file

@ -49,6 +49,7 @@ exports[`LatestVersionItem matches snapshot 1`] = `
className="tile is-child box" className="tile is-child box"
> >
<JSONEditor <JSONEditor
isFixedHeight={true}
name="schema" name="schema"
readOnly={true} readOnly={true}
showGutter={false} showGutter={false}

View file

@ -10,6 +10,7 @@ exports[`SchemaVersion matches snapshot 1`] = `
</td> </td>
<td> <td>
<JSONEditor <JSONEditor
isFixedHeight={true}
name="schema" name="schema"
readOnly={true} readOnly={true}
showGutter={false} showGutter={false}

View file

@ -43,7 +43,7 @@ const Edit = ({
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting, isDirty },
control, control,
} = useForm<NewSchemaSubjectRaw>({ mode: 'onChange' }); } = useForm<NewSchemaSubjectRaw>({ mode: 'onChange' });
@ -62,6 +62,7 @@ const Edit = ({
compatibilityLevel: CompatibilityLevelCompatibilityEnum; compatibilityLevel: CompatibilityLevelCompatibilityEnum;
newSchema: string; newSchema: string;
}) => { }) => {
try {
await updateSchema( await updateSchema(
schema, schema,
newSchema, newSchema,
@ -71,6 +72,9 @@ const Edit = ({
subject subject
); );
history.push(clusterSchemaPath(clusterName, subject)); history.push(clusterSchemaPath(clusterName, subject));
} catch (e) {
// do not redirect
}
}, },
[schema, register, control, clusterName, subject, updateSchema, history] [schema, register, control, clusterName, subject, updateSchema, history]
); );
@ -96,7 +100,7 @@ const Edit = ({
</div> </div>
</div> </div>
{schemasAreFetched && !isSubmitting ? ( {schemasAreFetched ? (
<div className="box"> <div className="box">
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@ -111,6 +115,7 @@ const Edit = ({
required: 'Schema Type is required.', required: 'Schema Type is required.',
})} })}
defaultValue={schema.schemaType} defaultValue={schema.schemaType}
disabled={isSubmitting}
> >
{Object.keys(SchemaType).map((type: string) => ( {Object.keys(SchemaType).map((type: string) => (
<option key={type} value={type}> <option key={type} value={type}>
@ -128,6 +133,7 @@ const Edit = ({
name="compatibilityLevel" name="compatibilityLevel"
ref={register()} ref={register()}
defaultValue={schema.compatibilityLevel} defaultValue={schema.compatibilityLevel}
disabled={isSubmitting}
> >
{Object.keys(CompatibilityLevelCompatibilityEnum).map( {Object.keys(CompatibilityLevelCompatibilityEnum).map(
(level: string) => ( (level: string) => (
@ -143,7 +149,9 @@ const Edit = ({
<div className="column is-one-half"> <div className="column is-one-half">
<h4 className="title is-5 mb-2">Latest Schema</h4> <h4 className="title is-5 mb-2">Latest Schema</h4>
<JSONEditor <JSONEditor
isFixedHeight
readOnly readOnly
height="500px"
value={getFormattedSchema()} value={getFormattedSchema()}
name="latestSchema" name="latestSchema"
highlightActiveLine={false} highlightActiveLine={false}
@ -154,8 +162,10 @@ const Edit = ({
<Controller <Controller
control={control} control={control}
name="newSchema" name="newSchema"
disabled={isSubmitting}
render={({ name, onChange }) => ( render={({ name, onChange }) => (
<JSONEditor <JSONEditor
readOnly={isSubmitting}
defaultValue={getFormattedSchema()} defaultValue={getFormattedSchema()}
name={name} name={name}
onChange={onChange} onChange={onChange}
@ -164,7 +174,11 @@ const Edit = ({
/> />
</div> </div>
</div> </div>
<button type="submit" className="button is-primary"> <button
type="submit"
className="button is-primary"
disabled={!isDirty || isSubmitting}
>
Submit Submit
</button> </button>
</form> </form>

View file

@ -48,6 +48,7 @@ exports[`Edit Component when schemas are fetched matches the snapshot 1`] = `
> >
<select <select
defaultValue="AVRO" defaultValue="AVRO"
disabled={false}
name="schemaType" name="schemaType"
> >
<option <option
@ -84,6 +85,7 @@ exports[`Edit Component when schemas are fetched matches the snapshot 1`] = `
> >
<select <select
defaultValue="BACKWARD" defaultValue="BACKWARD"
disabled={false}
name="compatibilityLevel" name="compatibilityLevel"
> >
<option <option
@ -143,7 +145,9 @@ exports[`Edit Component when schemas are fetched matches the snapshot 1`] = `
Latest Schema Latest Schema
</h4> </h4>
<JSONEditor <JSONEditor
height="500px"
highlightActiveLine={false} highlightActiveLine={false}
isFixedHeight={true}
name="latestSchema" name="latestSchema"
readOnly={true} readOnly={true}
value="{ value="{
@ -261,6 +265,7 @@ exports[`Edit Component when schemas are fetched matches the snapshot 1`] = `
"watchInternal": [Function], "watchInternal": [Function],
} }
} }
disabled={false}
name="newSchema" name="newSchema"
render={[Function]} render={[Function]}
/> />
@ -268,6 +273,7 @@ exports[`Edit Component when schemas are fetched matches the snapshot 1`] = `
</div> </div>
<button <button
className="button is-primary" className="button is-primary"
disabled={true}
type="submit" type="submit"
> >
Submit Submit

View file

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

View file

@ -4,15 +4,25 @@ import 'ace-builds/src-noconflict/mode-json5';
import 'ace-builds/src-noconflict/theme-textmate'; import 'ace-builds/src-noconflict/theme-textmate';
import React from 'react'; import React from 'react';
const JSONEditor: React.FC<IAceEditorProps> = (props) => ( interface JSONEditorProps extends IAceEditorProps {
isFixedHeight?: boolean;
}
const JSONEditor: React.FC<JSONEditorProps> = (props) => {
const { isFixedHeight, value } = props;
return (
<AceEditor <AceEditor
mode="json5" mode="json5"
theme="textmate" theme="textmate"
tabSize={2} tabSize={2}
width="100%" width="100%"
height={
isFixedHeight ? `${(value?.split('\n').length || 32) * 16}px` : '500px'
}
wrapEnabled wrapEnabled
{...props} {...props}
/> />
); );
};
export default JSONEditor; export default JSONEditor;

View file

@ -122,53 +122,22 @@ describe('Thunks', () => {
expect(error.status).toEqual(404); expect(error.status).toEqual(404);
expect(store.getActions()).toEqual([ expect(store.getActions()).toEqual([
actions.createSchemaAction.request(), actions.createSchemaAction.request(),
actions.createSchemaAction.failure({}), actions.createSchemaAction.failure({
alert: {
response: {
body: undefined,
status: 404,
statusText: 'Not Found',
},
subject: 'schema-NewSchema',
title: 'Schema NewSchema',
},
}),
]); ]);
} }
}); });
}); });
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(
@ -203,7 +172,7 @@ describe('Thunks', () => {
}); });
describe('updateSchema', () => { describe('updateSchema', () => {
it('calls createSchema', () => { it('calls PATCH_SCHEMA__REQUEST', () => {
store.dispatch( store.dispatch(
thunks.updateSchema( thunks.updateSchema(
fixtures.schema, fixtures.schema,
@ -215,23 +184,7 @@ describe('Thunks', () => {
) )
); );
expect(store.getActions()).toEqual([ expect(store.getActions()).toEqual([
actions.createSchemaAction.request(), actions.updateSchemaAction.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(),
]); ]);
}); });
}); });

View file

@ -130,11 +130,11 @@ export const createSchemaAction = createAsyncAction(
'POST_SCHEMA__FAILURE' 'POST_SCHEMA__FAILURE'
)<undefined, SchemaSubject, { alert?: FailurePayload }>(); )<undefined, SchemaSubject, { alert?: FailurePayload }>();
export const updateSchemaCompatibilityLevelAction = createAsyncAction( export const updateSchemaAction = createAsyncAction(
'PATCH_SCHEMA_COMPATIBILITY__REQUEST', 'PATCH_SCHEMA__REQUEST',
'PATCH_SCHEMA_COMPATIBILITY__SUCCESS', 'PATCH_SCHEMA__SUCCESS',
'PATCH_SCHEMA_COMPATIBILITY__FAILURE' 'PATCH_SCHEMA__FAILURE'
)<undefined, undefined, { alert?: FailurePayload }>(); )<undefined, SchemaSubject, { alert?: FailurePayload }>();
export const deleteSchemaAction = createAsyncAction( export const deleteSchemaAction = createAsyncAction(
'DELETE_SCHEMA__REQUEST', 'DELETE_SCHEMA__REQUEST',

View file

@ -68,32 +68,7 @@ export const createSchema = (
response, response,
}; };
dispatch(actions.createSchemaAction.failure({ alert })); dispatch(actions.createSchemaAction.failure({ alert }));
} throw error;
};
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 }));
} }
}; };
@ -105,27 +80,42 @@ export const updateSchema = (
clusterName: string, clusterName: string,
subject: string subject: string
): PromiseThunkResult => async (dispatch) => { ): PromiseThunkResult => async (dispatch) => {
dispatch(actions.updateSchemaAction.request());
try {
let schema: SchemaSubject = latestSchema;
if ( if (
(newSchema && (newSchema &&
!isEqual(JSON.parse(latestSchema.schema), JSON.parse(newSchema))) || !isEqual(JSON.parse(latestSchema.schema), JSON.parse(newSchema))) ||
newSchemaType !== latestSchema.schemaType newSchemaType !== latestSchema.schemaType
) { ) {
await dispatch( schema = await schemasApiClient.createNewSchema({
createSchema(clusterName, { clusterName,
newSchemaSubject: {
...latestSchema, ...latestSchema,
schema: newSchema || latestSchema.schema, schema: newSchema || latestSchema.schema,
schemaType: newSchemaType || latestSchema.schemaType, schemaType: newSchemaType || latestSchema.schemaType,
}) },
); });
} }
if (newCompatibilityLevel !== latestSchema.compatibilityLevel) { if (newCompatibilityLevel !== latestSchema.compatibilityLevel) {
await dispatch( await schemasApiClient.updateSchemaCompatibilityLevel({
updateSchemaCompatibilityLevel(
clusterName, clusterName,
subject, subject,
newCompatibilityLevel compatibilityLevel: {
) compatibility: newCompatibilityLevel,
); },
});
}
actions.updateSchemaAction.success(schema);
} catch (e) {
const response = await getResponse(e);
const alert: FailurePayload = {
subject: ['schema', subject].join('-'),
title: `Schema ${subject}`,
response,
};
dispatch(actions.updateSchemaAction.failure({ alert }));
throw e;
} }
}; };
export const deleteSchema = ( export const deleteSchema = (

View file

@ -66,6 +66,8 @@ const reducer = (state = initialState, action: Action): SchemasState => {
return { ...state, currentSchemaVersions: action.payload }; return { ...state, currentSchemaVersions: action.payload };
case 'POST_SCHEMA__SUCCESS': case 'POST_SCHEMA__SUCCESS':
return addToSchemaList(state, action.payload); return addToSchemaList(state, action.payload);
case 'PATCH_SCHEMA__SUCCESS':
return addToSchemaList(state, action.payload);
case getType(actions.deleteSchemaAction.success): case getType(actions.deleteSchemaAction.success):
return deleteFromSchemaList(state, action.payload); return deleteFromSchemaList(state, action.payload);
default: default: