Переглянути джерело

[Fixed issue/1449] Refactored ksqlDb reducer to reduxjs/toolkit approach (#1530)

* refactored ksqlDb reducer to reduxjs/toolkit approach

* fixed prettier requirement

* added ksql Execution test

* added new test
Denys Malofeiev 3 роки тому
батько
коміт
7ba6801b9c

+ 1 - 1
kafka-ui-react-app/src/components/KsqlDb/List/List.tsx

@@ -4,7 +4,7 @@ import ListItem from 'components/KsqlDb/List/ListItem';
 import React, { FC, useEffect } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
 import { useParams } from 'react-router';
-import { fetchKsqlDbTables } from 'redux/actions/thunks/ksqlDb';
+import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
 import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
 import { clusterKsqlDbQueryPath } from 'lib/paths';
 import PageHeading from 'components/common/PageHeading/PageHeading';

+ 4 - 2
kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx

@@ -5,11 +5,13 @@ import SQLEditor from 'components/common/SQLEditor/SQLEditor';
 import yup from 'lib/yupExtended';
 import { useForm, Controller } from 'react-hook-form';
 import { useParams } from 'react-router';
-import { executeKsql } from 'redux/actions/thunks/ksqlDb';
+import {
+  executeKsql,
+  resetExecutionResult,
+} from 'redux/reducers/ksqlDb/ksqlDbSlice';
 import ResultRenderer from 'components/KsqlDb/Query/ResultRenderer';
 import { useDispatch, useSelector } from 'react-redux';
 import { getKsqlExecution } from 'redux/reducers/ksqlDb/selectors';
-import { resetExecutionResult } from 'redux/actions';
 import { Button } from 'components/common/Button/Button';
 
 import {

+ 4 - 4
kafka-ui-react-app/src/components/KsqlDb/Query/__test__/Query.spec.tsx

@@ -21,8 +21,8 @@ describe('KsqlDb Query Component', () => {
         tables: [],
         executionResult: ksqlCommandResponse,
       },
-      legacyLoader: {
-        EXECUTE_KSQL: 'fetched',
+      loader: {
+        'ksqlDb/executeKsql': 'fulfilled',
       },
     };
     const store = mockStore(initialState);
@@ -50,8 +50,8 @@ describe('KsqlDb Query Component', () => {
           message: 'No available data',
         },
       },
-      legacyLoader: {
-        EXECUTE_KSQL: 'fetched',
+      loader: {
+        'ksqlDb/executeKsql': 'fulfilled',
       },
     };
     const store = mockStore(initialState);

+ 0 - 23
kafka-ui-react-app/src/redux/actions/__test__/actions.spec.ts

@@ -9,7 +9,6 @@ import {
   topicMessagePayload,
   topicMessagesMetaPayload,
 } from 'redux/reducers/topicMessages/__test__/fixtures';
-import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixtures';
 
 import { mockTopicsState } from './fixtures';
 
@@ -202,25 +201,3 @@ describe('Actions', () => {
     });
   });
 });
-
-describe('ksqlDb', () => {
-  it('creates GET_KSQL_DB_TABLES_AND_STREAMS__REQUEST', () => {
-    expect(actions.fetchKsqlDbTablesAction.request()).toEqual({
-      type: 'GET_KSQL_DB_TABLES_AND_STREAMS__REQUEST',
-    });
-  });
-  it('creates GET_KSQL_DB_TABLES_AND_STREAMS__SUCCESS', () => {
-    expect(
-      actions.fetchKsqlDbTablesAction.success(fetchKsqlDbTablesPayload)
-    ).toEqual({
-      type: 'GET_KSQL_DB_TABLES_AND_STREAMS__SUCCESS',
-      payload: fetchKsqlDbTablesPayload,
-    });
-  });
-  it('creates GET_KSQL_DB_TABLES_AND_STREAMS__FAILURE', () => {
-    expect(actions.fetchKsqlDbTablesAction.failure({})).toEqual({
-      type: 'GET_KSQL_DB_TABLES_AND_STREAMS__FAILURE',
-      payload: {},
-    });
-  });
-});

+ 0 - 53
kafka-ui-react-app/src/redux/actions/__test__/thunks/ksqlDb.spec.ts

@@ -1,53 +0,0 @@
-import fetchMock from 'fetch-mock-jest';
-import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
-import * as thunks from 'redux/actions/thunks';
-import * as actions from 'redux/actions';
-import { ksqlCommandResponse } from 'redux/reducers/ksqlDb/__test__/fixtures';
-import { transformKsqlResponse } from 'redux/actions/thunks';
-
-const store = mockStoreCreator;
-const clusterName = 'local';
-
-describe('Thunks', () => {
-  afterEach(() => {
-    fetchMock.restore();
-    store.clearActions();
-  });
-
-  describe('fetchKsqlDbTables', () => {
-    it('creates GET_KSQL_DB_TABLES_AND_STREAMS__SUCCESS when fetching streams', async () => {
-      fetchMock.post(`/api/clusters/${clusterName}/ksql`, ksqlCommandResponse);
-
-      await store.dispatch(thunks.fetchKsqlDbTables(clusterName));
-
-      expect(store.getActions()).toEqual([
-        actions.fetchKsqlDbTablesAction.request(),
-        actions.fetchKsqlDbTablesAction.success({
-          streams: transformKsqlResponse(ksqlCommandResponse.data),
-          tables: transformKsqlResponse(ksqlCommandResponse.data),
-        }),
-      ]);
-    });
-
-    it('creates GET_KSQL_DB_TABLES_AND_STREAMS__FAILURE', async () => {
-      fetchMock.post(`/api/clusters/${clusterName}/ksql`, 422);
-
-      await store.dispatch(thunks.fetchKsqlDbTables(clusterName));
-
-      expect(store.getActions()).toEqual([
-        actions.fetchKsqlDbTablesAction.request(),
-        actions.fetchKsqlDbTablesAction.failure({
-          alert: {
-            subject: 'ksqlDb',
-            title: 'Failed to fetch tables and streams',
-            response: {
-              status: 422,
-              statusText: 'Unprocessable Entity',
-              url: `/api/clusters/${clusterName}/ksql`,
-            },
-          },
-        }),
-      ]);
-    });
-  });
-});

+ 0 - 22
kafka-ui-react-app/src/redux/actions/actions.ts

@@ -15,7 +15,6 @@ import {
   TopicMessage,
   TopicMessageConsuming,
   TopicMessageSchema,
-  KsqlCommandResponse,
 } from 'generated-sources';
 
 export const fetchTopicsListAction = createAsyncAction(
@@ -186,24 +185,3 @@ export const updateTopicReplicationFactorAction = createAsyncAction(
   'UPDATE_REPLICATION_FACTOR__SUCCESS',
   'UPDATE_REPLICATION_FACTOR__FAILURE'
 )<undefined, undefined, { alert?: FailurePayload }>();
-
-export const fetchKsqlDbTablesAction = createAsyncAction(
-  'GET_KSQL_DB_TABLES_AND_STREAMS__REQUEST',
-  'GET_KSQL_DB_TABLES_AND_STREAMS__SUCCESS',
-  'GET_KSQL_DB_TABLES_AND_STREAMS__FAILURE'
-)<
-  undefined,
-  {
-    tables: Dictionary<string>[];
-    streams: Dictionary<string>[];
-  },
-  { alert?: FailurePayload }
->();
-
-export const executeKsqlAction = createAsyncAction(
-  'EXECUTE_KSQL__REQUEST',
-  'EXECUTE_KSQL__SUCCESS',
-  'EXECUTE_KSQL__FAILURE'
-)<undefined, KsqlCommandResponse, { alert?: FailurePayload }>();
-
-export const resetExecutionResult = createAction('RESET_EXECUTE_KSQL')();

+ 0 - 1
kafka-ui-react-app/src/redux/actions/thunks/index.ts

@@ -1,3 +1,2 @@
 export * from './topics';
 export * from './connectors';
-export * from './ksqlDb';

+ 0 - 88
kafka-ui-react-app/src/redux/actions/thunks/ksqlDb.ts

@@ -1,88 +0,0 @@
-import {
-  Configuration,
-  ExecuteKsqlCommandRequest,
-  KsqlApi,
-  Table as KsqlTable,
-} from 'generated-sources';
-import {
-  PromiseThunkResult,
-  ClusterName,
-  FailurePayload,
-} from 'redux/interfaces';
-import { BASE_PARAMS } from 'lib/constants';
-import * as actions from 'redux/actions/actions';
-import { getResponse } from 'lib/errorHandling';
-
-const apiClientConf = new Configuration(BASE_PARAMS);
-export const ksqlDbApiClient = new KsqlApi(apiClientConf);
-
-export const transformKsqlResponse = (
-  rawTable: Required<KsqlTable>
-): Dictionary<string>[] =>
-  rawTable.rows.map((row) =>
-    row.reduce(
-      (res, acc, index) => ({
-        ...res,
-        [rawTable.headers[index]]: acc,
-      }),
-      {} as Dictionary<string>
-    )
-  );
-
-const getTables = (clusterName: ClusterName) =>
-  ksqlDbApiClient.executeKsqlCommand({
-    clusterName,
-    ksqlCommand: { ksql: 'SHOW TABLES;' },
-  });
-
-const getStreams = (clusterName: ClusterName) =>
-  ksqlDbApiClient.executeKsqlCommand({
-    clusterName,
-    ksqlCommand: { ksql: 'SHOW STREAMS;' },
-  });
-
-export const fetchKsqlDbTables =
-  (clusterName: ClusterName): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.fetchKsqlDbTablesAction.request());
-    try {
-      const tables = await getTables(clusterName);
-      const streams = await getStreams(clusterName);
-
-      dispatch(
-        actions.fetchKsqlDbTablesAction.success({
-          tables: tables.data ? transformKsqlResponse(tables.data) : [],
-          streams: streams.data ? transformKsqlResponse(streams.data) : [],
-        })
-      );
-    } catch (error) {
-      const response = await getResponse(error);
-      const alert: FailurePayload = {
-        subject: 'ksqlDb',
-        title: `Failed to fetch tables and streams`,
-        response,
-      };
-
-      dispatch(actions.fetchKsqlDbTablesAction.failure({ alert }));
-    }
-  };
-
-export const executeKsql =
-  (params: ExecuteKsqlCommandRequest): PromiseThunkResult =>
-  async (dispatch) => {
-    dispatch(actions.executeKsqlAction.request());
-    try {
-      const response = await ksqlDbApiClient.executeKsqlCommand(params);
-
-      dispatch(actions.executeKsqlAction.success(response));
-    } catch (error) {
-      const response = await getResponse(error);
-      const alert: FailurePayload = {
-        subject: 'ksql execution',
-        title: `Failed to execute command ${params.ksqlCommand?.ksql}`,
-        response,
-      };
-
-      dispatch(actions.executeKsqlAction.failure({ alert }));
-    }
-  };

+ 1 - 1
kafka-ui-react-app/src/redux/reducers/index.ts

@@ -9,7 +9,7 @@ import topics from './topics/reducer';
 import topicMessages from './topicMessages/reducer';
 import consumerGroups from './consumerGroups/consumerGroupsSlice';
 import connect from './connect/reducer';
-import ksqlDb from './ksqlDb/reducer';
+import ksqlDb from './ksqlDb/ksqlDbSlice';
 import legacyLoader from './loader/reducer';
 import legacyAlerts from './alerts/reducer';
 

+ 45 - 0
kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/__snapshots__/reducer.spec.ts.snap

@@ -1,5 +1,50 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`KsqlDb reducer Exexute ksql and get result 1`] = `
+Object {
+  "executionResult": Object {
+    "streams": Array [
+      Object {
+        "isWindowed": "false",
+        "keyFormat": "KAFKA",
+        "name": "KSQL_PROCESSING_LOG",
+        "topic": "default_ksql_processing_log",
+        "type": "STREAM",
+        "valueFormat": "JSON",
+      },
+      Object {
+        "isWindowed": "false",
+        "keyFormat": "KAFKA",
+        "name": "PAGEVIEWS",
+        "topic": "pageviews",
+        "type": "STREAM",
+        "valueFormat": "AVRO",
+      },
+    ],
+    "tables": Array [
+      Object {
+        "isWindowed": "false",
+        "keyFormat": "KAFKA",
+        "name": "USERS",
+        "topic": "users",
+        "type": "TABLE",
+        "valueFormat": "AVRO",
+      },
+      Object {
+        "isWindowed": "false",
+        "keyFormat": "KAFKA",
+        "name": "USERS2",
+        "topic": "users",
+        "type": "TABLE",
+        "valueFormat": "AVRO",
+      },
+    ],
+  },
+  "streams": Array [],
+  "tables": Array [],
+}
+`;
+
 exports[`KsqlDb reducer Fetches tables and streams 1`] = `
 Object {
   "executionResult": null,

+ 37 - 7
kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/reducer.spec.ts

@@ -1,23 +1,53 @@
-import { fetchKsqlDbTablesAction, resetExecutionResult } from 'redux/actions';
-import reducer, { initialState } from 'redux/reducers/ksqlDb/reducer';
+import reducer, {
+  initialState,
+  fetchKsqlDbTables,
+  resetExecutionResult,
+  executeKsql,
+  transformKsqlResponse,
+} from 'redux/reducers/ksqlDb/ksqlDbSlice';
+import { Table } from 'generated-sources';
 
 import { fetchKsqlDbTablesPayload } from './fixtures';
 
 describe('KsqlDb reducer', () => {
   it('returns the initial state', () => {
-    expect(reducer(undefined, fetchKsqlDbTablesAction.request())).toEqual(
+    expect(reducer(undefined, { type: fetchKsqlDbTables.pending })).toEqual(
       initialState
     );
   });
+
+  it('It should transform data with given headers and rows', () => {
+    const data: Table = {
+      headers: ['header1'],
+      rows: [['value1'], ['value2'], ['value3']],
+    };
+    const transformedData = transformKsqlResponse(data);
+    expect(transformedData).toEqual([
+      { header1: 'value1' },
+      { header1: 'value2' },
+      { header1: 'value3' },
+    ]);
+  });
+
   it('Fetches tables and streams', () => {
-    const state = reducer(
-      undefined,
-      fetchKsqlDbTablesAction.success(fetchKsqlDbTablesPayload)
-    );
+    const state = reducer(undefined, {
+      type: fetchKsqlDbTables.fulfilled,
+      payload: fetchKsqlDbTablesPayload,
+    });
     expect(state.tables.length).toEqual(2);
     expect(state.streams.length).toEqual(2);
     expect(state).toMatchSnapshot();
   });
+
+  it('Exexute ksql and get result', () => {
+    const state = reducer(undefined, {
+      type: executeKsql.fulfilled,
+      payload: fetchKsqlDbTablesPayload,
+    });
+    expect(state.executionResult).toBeTruthy();
+    expect(state).toMatchSnapshot();
+  });
+
   it('Resets execution result', () => {
     const state = reducer(
       {

+ 12 - 2
kafka-ui-react-app/src/redux/reducers/ksqlDb/__test__/selectors.spec.ts

@@ -1,11 +1,18 @@
 import { store } from 'redux/store';
 import * as selectors from 'redux/reducers/ksqlDb/selectors';
-import { fetchKsqlDbTablesAction } from 'redux/actions';
+import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
 
 import { fetchKsqlDbTablesPayload } from './fixtures';
 
 describe('TopicMessages selectors', () => {
   describe('Initial state', () => {
+    beforeAll(() => {
+      store.dispatch({
+        type: fetchKsqlDbTables.pending.type,
+        payload: fetchKsqlDbTablesPayload,
+      });
+    });
+
     it('Returns empty state', () => {
       expect(selectors.getKsqlDbTables(store.getState())).toEqual({
         rows: [],
@@ -19,7 +26,10 @@ describe('TopicMessages selectors', () => {
 
   describe('State', () => {
     beforeAll(() => {
-      store.dispatch(fetchKsqlDbTablesAction.success(fetchKsqlDbTablesPayload));
+      store.dispatch({
+        type: fetchKsqlDbTables.fulfilled.type,
+        payload: fetchKsqlDbTablesPayload,
+      });
     });
 
     it('Returns tables and streams', () => {

+ 88 - 0
kafka-ui-react-app/src/redux/reducers/ksqlDb/ksqlDbSlice.ts

@@ -0,0 +1,88 @@
+import { KsqlState } from 'redux/interfaces/ksqlDb';
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import { BASE_PARAMS } from 'lib/constants';
+import {
+  Configuration,
+  ExecuteKsqlCommandRequest,
+  KsqlApi,
+  Table as KsqlTable,
+} from 'generated-sources';
+import { ClusterName } from 'redux/interfaces';
+
+const apiClientConf = new Configuration(BASE_PARAMS);
+export const ksqlDbApiClient = new KsqlApi(apiClientConf);
+
+export const transformKsqlResponse = (
+  rawTable: Required<KsqlTable>
+): Dictionary<string>[] =>
+  rawTable.rows.map((row) =>
+    row.reduce(
+      (res, acc, index) => ({
+        ...res,
+        [rawTable.headers[index]]: acc,
+      }),
+      {} as Dictionary<string>
+    )
+  );
+
+const getTables = (clusterName: ClusterName) =>
+  ksqlDbApiClient.executeKsqlCommand({
+    clusterName,
+    ksqlCommand: { ksql: 'SHOW TABLES;' },
+  });
+
+const getStreams = (clusterName: ClusterName) =>
+  ksqlDbApiClient.executeKsqlCommand({
+    clusterName,
+    ksqlCommand: { ksql: 'SHOW STREAMS;' },
+  });
+
+export const fetchKsqlDbTables = createAsyncThunk(
+  'ksqlDb/fetchKsqlDbTables',
+  async (clusterName: ClusterName) => {
+    const tables = await getTables(clusterName);
+    const streams = await getStreams(clusterName);
+
+    return {
+      tables: tables.data ? transformKsqlResponse(tables.data) : [],
+      streams: streams.data ? transformKsqlResponse(streams.data) : [],
+    };
+  }
+);
+
+export const executeKsql = createAsyncThunk(
+  'ksqlDb/executeKsql',
+  (params: ExecuteKsqlCommandRequest) =>
+    ksqlDbApiClient.executeKsqlCommand(params)
+);
+
+export const initialState: KsqlState = {
+  streams: [],
+  tables: [],
+  executionResult: null,
+};
+
+export const ksqlDbSlice = createSlice({
+  name: 'ksqlDb',
+  initialState,
+  reducers: {
+    resetExecutionResult: (state) => ({
+      ...state,
+      executionResult: null,
+    }),
+  },
+  extraReducers: (builder) => {
+    builder.addCase(fetchKsqlDbTables.fulfilled, (state, action) => ({
+      ...state,
+      ...action.payload,
+    }));
+    builder.addCase(executeKsql.fulfilled, (state, action) => ({
+      ...state,
+      executionResult: action.payload,
+    }));
+  },
+});
+
+export const { resetExecutionResult } = ksqlDbSlice.actions;
+
+export default ksqlDbSlice.reducer;

+ 0 - 35
kafka-ui-react-app/src/redux/reducers/ksqlDb/reducer.ts

@@ -1,35 +0,0 @@
-import { Action } from 'redux/interfaces';
-import { getType } from 'typesafe-actions';
-import * as actions from 'redux/actions';
-import { KsqlState } from 'redux/interfaces/ksqlDb';
-
-export const initialState: KsqlState = {
-  streams: [],
-  tables: [],
-  executionResult: null,
-};
-
-// eslint-disable-next-line @typescript-eslint/default-param-last
-const reducer = (state = initialState, action: Action): KsqlState => {
-  switch (action.type) {
-    case getType(actions.fetchKsqlDbTablesAction.success):
-      return {
-        ...state,
-        ...action.payload,
-      };
-    case getType(actions.executeKsqlAction.success):
-      return {
-        ...state,
-        executionResult: action.payload,
-      };
-    case getType(actions.resetExecutionResult):
-      return {
-        ...state,
-        executionResult: null,
-      };
-    default:
-      return state;
-  }
-};
-
-export default reducer;

+ 9 - 8
kafka-ui-react-app/src/redux/reducers/ksqlDb/selectors.ts

@@ -1,21 +1,22 @@
 import { createSelector } from '@reduxjs/toolkit';
 import { RootState } from 'redux/interfaces';
-import { createLeagcyFetchingSelector } from 'redux/reducers/loader/selectors';
+import { createFetchingSelector } from 'redux/reducers/loader/selectors';
 import { KsqlState } from 'redux/interfaces/ksqlDb';
 
 const ksqlDbState = ({ ksqlDb }: RootState): KsqlState => ksqlDb;
 
-const getKsqlDbFetchTablesAndStreamsFetchingStatus =
-  createLeagcyFetchingSelector('GET_KSQL_DB_TABLES_AND_STREAMS');
+const getKsqlDbFetchTablesAndStreamsFetchingStatus = createFetchingSelector(
+  'ksqlDb/fetchKsqlDbTables'
+);
 
-const getKsqlExecutionStatus = createLeagcyFetchingSelector('EXECUTE_KSQL');
+const getKsqlExecutionStatus = createFetchingSelector('ksqlDb/executeKsql');
 
 export const getKsqlDbTables = createSelector(
   [ksqlDbState, getKsqlDbFetchTablesAndStreamsFetchingStatus],
   (state, status) => ({
     rows: [...state.streams, ...state.tables],
-    fetched: status === 'fetched',
-    fetching: status === 'fetching' || status === 'notFetched',
+    fetched: status === 'fulfilled',
+    fetching: status === 'pending',
     tablesCount: state.tables.length,
     streamsCount: state.streams.length,
   })
@@ -25,7 +26,7 @@ export const getKsqlExecution = createSelector(
   [ksqlDbState, getKsqlExecutionStatus],
   (state, status) => ({
     executionResult: state.executionResult,
-    fetched: status === 'fetched',
-    fetching: status === 'fetching',
+    fetched: status === 'fulfilled',
+    fetching: status === 'pending',
   })
 );