From 2a2e81d18f2cfc5df2f2e5eb36c542e5906fc684 Mon Sep 17 00:00:00 2001 From: Alexander Krivonosov <31561808+GneyHabub@users.noreply.github.com> Date: Sat, 1 May 2021 09:09:02 +0300 Subject: [PATCH] Implement updating registry compatibility level (#391) * Implement updating registry compatibility level --- .../Schemas/List/GlobalSchemaSelector.tsx | 78 +++++++++++++++ .../src/components/Schemas/List/List.tsx | 28 +++++- .../components/Schemas/List/ListContainer.tsx | 14 ++- .../Schemas/List/__test__/List.spec.tsx | 3 + .../actions/__test__/thunks/schemas.spec.ts | 97 +++++++++++++++++++ .../src/redux/actions/actions.ts | 13 +++ .../src/redux/actions/thunks/schemas.ts | 38 ++++++++ .../src/redux/interfaces/schema.ts | 7 +- .../reducers/schemas/__test__/reducer.spec.ts | 41 +++++++- .../schemas/__test__/selectors.spec.ts | 16 +++ .../src/redux/reducers/schemas/reducer.ts | 4 + .../src/redux/reducers/schemas/selectors.ts | 11 +++ 12 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector.tsx diff --git a/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector.tsx b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector.tsx new file mode 100644 index 00000000000..0b8e7c85272 --- /dev/null +++ b/kafka-ui-react-app/src/components/Schemas/List/GlobalSchemaSelector.tsx @@ -0,0 +1,78 @@ +import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; +import PageLoader from 'components/common/PageLoader/PageLoader'; +import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; +import { ClusterName } from 'redux/interfaces'; + +export interface GlobalSchemaSelectorProps { + globalSchemaCompatibilityLevel?: CompatibilityLevelCompatibilityEnum; + updateGlobalSchemaCompatibilityLevel: ( + clusterName: ClusterName, + compatibilityLevel: CompatibilityLevelCompatibilityEnum + ) => Promise; +} + +const GlobalSchemaSelector: React.FC = ({ + globalSchemaCompatibilityLevel, + updateGlobalSchemaCompatibilityLevel, +}) => { + const { clusterName } = useParams<{ clusterName: string }>(); + + const { + register, + handleSubmit, + formState: { isSubmitting }, + } = useForm(); + + const [ + isUpdateCompatibilityConfirmationVisible, + setUpdateCompatibilityConfirmationVisible, + ] = React.useState(false); + + const onCompatibilityLevelUpdate = async ({ + compatibilityLevel, + }: { + compatibilityLevel: CompatibilityLevelCompatibilityEnum; + }) => { + await updateGlobalSchemaCompatibilityLevel(clusterName, compatibilityLevel); + setUpdateCompatibilityConfirmationVisible(false); + }; + + return ( +
+
Global Compatibility Level:
+
+ +
+ setUpdateCompatibilityConfirmationVisible(false)} + onConfirm={handleSubmit(onCompatibilityLevelUpdate)} + > + {isSubmitting ? ( + + ) : ( + `Are you sure you want to update the global compatibility level? + This may affect the compatibility levels of the schemas.` + )} + +
+ ); +}; + +export default GlobalSchemaSelector; diff --git a/kafka-ui-react-app/src/components/Schemas/List/List.tsx b/kafka-ui-react-app/src/components/Schemas/List/List.tsx index fd7abd00852..8b5082077b9 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/List.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/List.tsx @@ -1,5 +1,8 @@ import React from 'react'; -import { SchemaSubject } from 'generated-sources'; +import { + CompatibilityLevelCompatibilityEnum, + SchemaSubject, +} from 'generated-sources'; import { Link, useParams } from 'react-router-dom'; import { clusterSchemaNewPath } from 'lib/paths'; import { ClusterName } from 'redux/interfaces'; @@ -8,23 +11,38 @@ import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import ClusterContext from 'components/contexts/ClusterContext'; import ListItem from './ListItem'; +import GlobalSchemaSelector from './GlobalSchemaSelector'; export interface ListProps { schemas: SchemaSubject[]; isFetching: boolean; + isGlobalSchemaCompatibilityLevelFetched: boolean; + globalSchemaCompatibilityLevel?: CompatibilityLevelCompatibilityEnum; fetchSchemasByClusterName: (clusterName: ClusterName) => void; + fetchGlobalSchemaCompatibilityLevel: ( + clusterName: ClusterName + ) => Promise; + updateGlobalSchemaCompatibilityLevel: ( + clusterName: ClusterName, + compatibilityLevel: CompatibilityLevelCompatibilityEnum + ) => Promise; } const List: React.FC = ({ schemas, isFetching, + globalSchemaCompatibilityLevel, + isGlobalSchemaCompatibilityLevelFetched, fetchSchemasByClusterName, + fetchGlobalSchemaCompatibilityLevel, + updateGlobalSchemaCompatibilityLevel, }) => { const { isReadOnly } = React.useContext(ClusterContext); const { clusterName } = useParams<{ clusterName: string }>(); React.useEffect(() => { fetchSchemasByClusterName(clusterName); + fetchGlobalSchemaCompatibilityLevel(clusterName); }, [fetchSchemasByClusterName, clusterName]); return ( @@ -32,8 +50,14 @@ const List: React.FC = ({ Schema Registry
- {!isReadOnly && ( + {!isReadOnly && isGlobalSchemaCompatibilityLevelFetched && (
+ ({ isFetching: getIsSchemaListFetching(state), schemas: getSchemaList(state), + globalSchemaCompatibilityLevel: getGlobalSchemaCompatibilityLevel(state), + isGlobalSchemaCompatibilityLevelFetched: getGlobalSchemaCompatibilityLevelFetched( + state + ), }); const mapDispatchToProps = { + fetchGlobalSchemaCompatibilityLevel, fetchSchemasByClusterName, + updateGlobalSchemaCompatibilityLevel, }; export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx b/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx index 0fbc6188f77..e37ffd9ecd1 100644 --- a/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/List/__test__/List.spec.tsx @@ -32,6 +32,9 @@ describe('List', () => { diff --git a/kafka-ui-react-app/src/redux/actions/__test__/thunks/schemas.spec.ts b/kafka-ui-react-app/src/redux/actions/__test__/thunks/schemas.spec.ts index 58bac177d61..902c306d9e5 100644 --- a/kafka-ui-react-app/src/redux/actions/__test__/thunks/schemas.spec.ts +++ b/kafka-ui-react-app/src/redux/actions/__test__/thunks/schemas.spec.ts @@ -188,4 +188,101 @@ describe('Thunks', () => { ]); }); }); + + describe('fetchGlobalSchemaCompatibilityLevel', () => { + it('calls GET_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST on the fucntion call', () => { + store.dispatch(thunks.fetchGlobalSchemaCompatibilityLevel(clusterName)); + expect(store.getActions()).toEqual([ + actions.fetchGlobalSchemaCompatibilityLevelAction.request(), + ]); + }); + + it('calls GET_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS on a successful API call', async () => { + fetchMock.getOnce(`/api/clusters/${clusterName}/schemas/compatibility`, { + compatibility: CompatibilityLevelCompatibilityEnum.FORWARD, + }); + await store.dispatch( + thunks.fetchGlobalSchemaCompatibilityLevel(clusterName) + ); + expect(store.getActions()).toEqual([ + actions.fetchGlobalSchemaCompatibilityLevelAction.request(), + actions.fetchGlobalSchemaCompatibilityLevelAction.success( + CompatibilityLevelCompatibilityEnum.FORWARD + ), + ]); + }); + + it('calls GET_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE on an unsuccessful API call', async () => { + fetchMock.getOnce( + `/api/clusters/${clusterName}/schemas/compatibility`, + 404 + ); + try { + await store.dispatch( + thunks.fetchGlobalSchemaCompatibilityLevel(clusterName) + ); + } catch (error) { + expect(error.status).toEqual(404); + expect(store.getActions()).toEqual([ + actions.fetchGlobalSchemaCompatibilityLevelAction.request(), + actions.fetchGlobalSchemaCompatibilityLevelAction.failure(), + ]); + } + }); + }); + + describe('updateGlobalSchemaCompatibilityLevel', () => { + const compatibilityLevel = CompatibilityLevelCompatibilityEnum.FORWARD; + it('calls POST_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST on the fucntion call', () => { + store.dispatch( + thunks.updateGlobalSchemaCompatibilityLevel( + clusterName, + compatibilityLevel + ) + ); + expect(store.getActions()).toEqual([ + actions.updateGlobalSchemaCompatibilityLevelAction.request(), + ]); + }); + + it('calls POST_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS on a successful API call', async () => { + fetchMock.putOnce( + `/api/clusters/${clusterName}/schemas/compatibility`, + 200 + ); + await store.dispatch( + thunks.updateGlobalSchemaCompatibilityLevel( + clusterName, + compatibilityLevel + ) + ); + expect(store.getActions()).toEqual([ + actions.updateGlobalSchemaCompatibilityLevelAction.request(), + actions.updateGlobalSchemaCompatibilityLevelAction.success( + CompatibilityLevelCompatibilityEnum.FORWARD + ), + ]); + }); + + it('calls POST_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE on an unsuccessful API call', async () => { + fetchMock.putOnce( + `/api/clusters/${clusterName}/schemas/compatibility`, + 404 + ); + try { + await store.dispatch( + thunks.updateGlobalSchemaCompatibilityLevel( + clusterName, + compatibilityLevel + ) + ); + } catch (error) { + expect(error.status).toEqual(404); + expect(store.getActions()).toEqual([ + actions.updateGlobalSchemaCompatibilityLevelAction.request(), + actions.updateGlobalSchemaCompatibilityLevelAction.failure(), + ]); + } + }); + }); }); diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts index b6f0b29d69e..9b9649e2199 100644 --- a/kafka-ui-react-app/src/redux/actions/actions.ts +++ b/kafka-ui-react-app/src/redux/actions/actions.ts @@ -16,6 +16,7 @@ import { ConsumerGroup, ConsumerGroupDetails, SchemaSubject, + CompatibilityLevelCompatibilityEnum, } from 'generated-sources'; export const fetchClusterStatsAction = createAsyncAction( @@ -118,6 +119,18 @@ export const fetchSchemasByClusterNameAction = createAsyncAction( 'GET_CLUSTER_SCHEMAS__FAILURE' )(); +export const fetchGlobalSchemaCompatibilityLevelAction = createAsyncAction( + 'GET_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST', + 'GET_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS', + 'GET_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE' +)(); + +export const updateGlobalSchemaCompatibilityLevelAction = createAsyncAction( + 'PUT_GLOBAL_SCHEMA_COMPATIBILITY__REQUEST', + 'PUT_GLOBAL_SCHEMA_COMPATIBILITY__SUCCESS', + 'PUT_GLOBAL_SCHEMA_COMPATIBILITY__FAILURE' +)(); + export const fetchSchemaVersionsAction = createAsyncAction( 'GET_SCHEMA_VERSIONS__REQUEST', 'GET_SCHEMA_VERSIONS__SUCCESS', diff --git a/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts b/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts index bea8cdb15a4..bc98688afbc 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts @@ -49,6 +49,44 @@ export const fetchSchemaVersions = ( } }; +export const fetchGlobalSchemaCompatibilityLevel = ( + clusterName: ClusterName +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.request()); + try { + const result = await schemasApiClient.getGlobalSchemaCompatibilityLevel({ + clusterName, + }); + dispatch( + actions.fetchGlobalSchemaCompatibilityLevelAction.success( + result.compatibility + ) + ); + } catch (e) { + dispatch(actions.fetchGlobalSchemaCompatibilityLevelAction.failure()); + } +}; + +export const updateGlobalSchemaCompatibilityLevel = ( + clusterName: ClusterName, + compatibilityLevel: CompatibilityLevelCompatibilityEnum +): PromiseThunkResult => async (dispatch) => { + dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.request()); + try { + await schemasApiClient.updateGlobalSchemaCompatibilityLevel({ + clusterName, + compatibilityLevel: { compatibility: compatibilityLevel }, + }); + dispatch( + actions.updateGlobalSchemaCompatibilityLevelAction.success( + compatibilityLevel + ) + ); + } catch (e) { + dispatch(actions.updateGlobalSchemaCompatibilityLevelAction.failure()); + } +}; + export const createSchema = ( clusterName: ClusterName, newSchemaSubject: NewSchemaSubject diff --git a/kafka-ui-react-app/src/redux/interfaces/schema.ts b/kafka-ui-react-app/src/redux/interfaces/schema.ts index 54f47baa06a..6a2392b9aad 100644 --- a/kafka-ui-react-app/src/redux/interfaces/schema.ts +++ b/kafka-ui-react-app/src/redux/interfaces/schema.ts @@ -1,4 +1,8 @@ -import { NewSchemaSubject, SchemaSubject } from 'generated-sources'; +import { + CompatibilityLevelCompatibilityEnum, + NewSchemaSubject, + SchemaSubject, +} from 'generated-sources'; export type SchemaName = string; @@ -6,6 +10,7 @@ export interface SchemasState { byName: { [subject: string]: SchemaSubject }; allNames: SchemaName[]; currentSchemaVersions: SchemaSubject[]; + globalSchemaCompatibilityLevel?: CompatibilityLevelCompatibilityEnum; } export interface NewSchemaSubjectRaw extends NewSchemaSubject { diff --git a/kafka-ui-react-app/src/redux/reducers/schemas/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/schemas/__test__/reducer.spec.ts index 1aa2602c37e..4a434fd9429 100644 --- a/kafka-ui-react-app/src/redux/reducers/schemas/__test__/reducer.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/schemas/__test__/reducer.spec.ts @@ -1,7 +1,12 @@ -import { SchemaSubject, SchemaType } from 'generated-sources'; +import { + CompatibilityLevelCompatibilityEnum, + SchemaSubject, + SchemaType, +} from 'generated-sources'; import { createSchemaAction, deleteSchemaAction, + fetchGlobalSchemaCompatibilityLevelAction, fetchSchemasByClusterNameAction, fetchSchemaVersionsAction, } from 'redux/actions'; @@ -76,4 +81,38 @@ describe('Schemas reducer', () => { currentSchemaVersions: [], }); }); + + it('adds global compatibility on successful fetch', () => { + expect( + reducer( + initialState, + fetchGlobalSchemaCompatibilityLevelAction.success( + CompatibilityLevelCompatibilityEnum.BACKWARD + ) + ) + ).toEqual({ + ...initialState, + globalSchemaCompatibilityLevel: + CompatibilityLevelCompatibilityEnum.BACKWARD, + }); + }); + + it('replaces global compatibility on successful update', () => { + expect( + reducer( + { + ...initialState, + globalSchemaCompatibilityLevel: + CompatibilityLevelCompatibilityEnum.FORWARD, + }, + fetchGlobalSchemaCompatibilityLevelAction.success( + CompatibilityLevelCompatibilityEnum.BACKWARD + ) + ) + ).toEqual({ + ...initialState, + globalSchemaCompatibilityLevel: + CompatibilityLevelCompatibilityEnum.BACKWARD, + }); + }); }); diff --git a/kafka-ui-react-app/src/redux/reducers/schemas/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/schemas/__test__/selectors.spec.ts index 7081baa23cd..2741c20ecee 100644 --- a/kafka-ui-react-app/src/redux/reducers/schemas/__test__/selectors.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/schemas/__test__/selectors.spec.ts @@ -1,11 +1,13 @@ import { orderBy } from 'lodash'; import { createSchemaAction, + fetchGlobalSchemaCompatibilityLevelAction, fetchSchemasByClusterNameAction, fetchSchemaVersionsAction, } from 'redux/actions'; import configureStore from 'redux/store/configureStore'; import * as selectors from 'redux/reducers/schemas/selectors'; +import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; import { clusterSchemasPayload, @@ -44,6 +46,11 @@ describe('Schemas selectors', () => { ); store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload)); store.dispatch(createSchemaAction.success(newSchemaPayload)); + store.dispatch( + fetchGlobalSchemaCompatibilityLevelAction.success( + CompatibilityLevelCompatibilityEnum.BACKWARD + ) + ); }); it('returns fetch status', () => { @@ -52,6 +59,9 @@ describe('Schemas selectors', () => { selectors.getIsSchemaVersionFetched(store.getState()) ).toBeTruthy(); expect(selectors.getSchemaCreated(store.getState())).toBeTruthy(); + expect( + selectors.getGlobalSchemaCompatibilityLevelFetched(store.getState()) + ).toBeTruthy(); }); it('returns schema list', () => { @@ -71,5 +81,11 @@ describe('Schemas selectors', () => { orderBy(schemaVersionsPayload, 'id', 'desc') ); }); + + it('return registry compatibility level', () => { + expect( + selectors.getGlobalSchemaCompatibilityLevel(store.getState()) + ).toEqual(CompatibilityLevelCompatibilityEnum.BACKWARD); + }); }); }); diff --git a/kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts b/kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts index f40d504cbd3..11f139fae1a 100644 --- a/kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts +++ b/kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts @@ -70,6 +70,10 @@ const reducer = (state = initialState, action: Action): SchemasState => { return addToSchemaList(state, action.payload); case getType(actions.deleteSchemaAction.success): return deleteFromSchemaList(state, action.payload); + case getType(actions.fetchGlobalSchemaCompatibilityLevelAction.success): + return { ...state, globalSchemaCompatibilityLevel: action.payload }; + case getType(actions.updateGlobalSchemaCompatibilityLevelAction.success): + return { ...state, globalSchemaCompatibilityLevel: action.payload }; default: return state; } diff --git a/kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts b/kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts index fc39a87f122..9c6e8f80eca 100644 --- a/kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts @@ -7,6 +7,8 @@ const schemasState = ({ schemas }: RootState): SchemasState => schemas; const getAllNames = (state: RootState) => schemasState(state).allNames; const getSchemaMap = (state: RootState) => schemasState(state).byName; +export const getGlobalSchemaCompatibilityLevel = (state: RootState) => + schemasState(state).globalSchemaCompatibilityLevel; const getSchemaListFetchingStatus = createFetchingSelector( 'GET_CLUSTER_SCHEMAS' @@ -18,11 +20,20 @@ const getSchemaVersionsFetchingStatus = createFetchingSelector( const getSchemaCreationStatus = createFetchingSelector('POST_SCHEMA'); +const getGlobalSchemaCompatibilityLevelFetchingStatus = createFetchingSelector( + 'GET_GLOBAL_SCHEMA_COMPATIBILITY' +); + export const getIsSchemaListFetched = createSelector( getSchemaListFetchingStatus, (status) => status === 'fetched' ); +export const getGlobalSchemaCompatibilityLevelFetched = createSelector( + getGlobalSchemaCompatibilityLevelFetchingStatus, + (status) => status === 'fetched' +); + export const getIsSchemaListFetching = createSelector( getSchemaListFetchingStatus, (status) => status === 'fetching' || status === 'notFetched'