From 9d75dbdacdd368cf76aad6e324176edbfaf9abbb Mon Sep 17 00:00:00 2001 From: Oleg Shur Date: Wed, 7 Apr 2021 23:50:17 +0300 Subject: [PATCH] Kafka Connect. Initial setup (#362) * Refactor Nav. Use feature flags. Connect * Refactor Alerts * Kafka Connect initial setup --- .../src/components/Cluster/Cluster.tsx | 57 +++++- .../Cluster/__tests__/Cluster.spec.tsx | 85 +++++++++ .../src/components/Connect/Connect.tsx | 16 ++ .../src/components/Connect/List/List.tsx | 87 ++++++++++ .../components/Connect/List/ListContainer.ts | 28 +++ .../Connect/List/__tests__/List.spec.tsx | 88 ++++++++++ .../src/components/Nav/ClusterMenu.tsx | 162 +++++++++--------- .../src/components/Nav/ClusterStatusIcon.tsx | 28 +++ .../src/components/Nav/DefaultClusterIcon.tsx | 22 +++ .../Nav/__tests__/ClusterMenu.spec.tsx | 36 ++++ .../Nav/__tests__/ClusterStatusIcon.spec.tsx | 22 +++ .../ClusterStatusIcon.spec.tsx.snap | 21 +++ .../Schemas/Details/__test__/Details.spec.tsx | 8 +- .../Schemas/List/__test__/List.spec.tsx | 8 +- .../Topics/List/__tests__/List.spec.tsx | 16 +- .../components/common/Dashboard/Indicator.tsx | 19 +- .../__snapshots__/Indicator.spec.tsx.snap | 2 +- .../src/components/contexts/ClusterContext.ts | 10 +- .../src/lib/__tests__/paths.spec.ts | 6 + kafka-ui-react-app/src/lib/paths.ts | 4 + .../__test__/thunks/connectors.spec.ts | 49 ++++++ .../actions/__test__/thunks/schemas.spec.ts | 18 +- .../actions/__test__/thunks/topics.spec.ts | 18 +- .../src/redux/actions/actions.ts | 20 ++- .../src/redux/actions/thunks/connectors.ts | 94 ++++++++++ .../src/redux/actions/thunks/index.ts | 1 + .../src/redux/actions/thunks/schemas.ts | 3 +- .../src/redux/actions/thunks/topics.ts | 3 +- .../src/redux/interfaces/alerts.ts | 3 +- .../src/redux/interfaces/connect.ts | 6 + .../src/redux/interfaces/index.ts | 3 + .../reducers/alerts/__test__/fixtures.ts | 10 +- .../reducers/alerts/__test__/reducer.spec.ts | 40 ++--- .../alerts/__test__/selectors.spec.ts | 10 +- .../src/redux/reducers/alerts/utils.ts | 10 +- .../reducers/clusters/__test__/fixtures.ts | 2 + .../src/redux/reducers/clusters/selectors.ts | 9 +- .../connect/__tests__/reducer.spec.ts | 24 +++ .../src/redux/reducers/connect/reducer.ts | 22 +++ .../src/redux/reducers/connect/selectors.ts | 35 ++++ .../src/redux/reducers/index.ts | 2 + .../store/configureStore/mockStoreCreator.ts | 14 ++ 42 files changed, 931 insertions(+), 190 deletions(-) create mode 100644 kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx create mode 100644 kafka-ui-react-app/src/components/Connect/Connect.tsx create mode 100644 kafka-ui-react-app/src/components/Connect/List/List.tsx create mode 100644 kafka-ui-react-app/src/components/Connect/List/ListContainer.ts create mode 100644 kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx create mode 100644 kafka-ui-react-app/src/components/Nav/ClusterStatusIcon.tsx create mode 100644 kafka-ui-react-app/src/components/Nav/DefaultClusterIcon.tsx create mode 100644 kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx create mode 100644 kafka-ui-react-app/src/components/Nav/__tests__/ClusterStatusIcon.spec.tsx create mode 100644 kafka-ui-react-app/src/components/Nav/__tests__/__snapshots__/ClusterStatusIcon.spec.tsx.snap create mode 100644 kafka-ui-react-app/src/redux/actions/__test__/thunks/connectors.spec.ts create mode 100644 kafka-ui-react-app/src/redux/actions/thunks/connectors.ts create mode 100644 kafka-ui-react-app/src/redux/interfaces/connect.ts create mode 100644 kafka-ui-react-app/src/redux/reducers/connect/__tests__/reducer.spec.ts create mode 100644 kafka-ui-react-app/src/redux/reducers/connect/reducer.ts create mode 100644 kafka-ui-react-app/src/redux/reducers/connect/selectors.ts create mode 100644 kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts diff --git a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx index 3461410e4d..fdf778c40b 100644 --- a/kafka-ui-react-app/src/components/Cluster/Cluster.tsx +++ b/kafka-ui-react-app/src/components/Cluster/Cluster.tsx @@ -1,29 +1,70 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { Switch, Route, Redirect, useParams } from 'react-router-dom'; -import BrokersContainer from 'components/Brokers/BrokersContainer'; +import { ClusterFeaturesEnum } from 'generated-sources'; +import { + getClustersFeatures, + getClustersReadonlyStatus, +} from 'redux/reducers/clusters/selectors'; +import { + clusterBrokersPath, + clusterConnectorsPath, + clusterConsumerGroupsPath, + clusterSchemasPath, + clusterTopicsPath, +} from 'lib/paths'; import Topics from 'components/Topics/Topics'; -import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer'; import Schemas from 'components/Schemas/Schemas'; -import { getClustersReadonlyStatus } from 'redux/reducers/clusters/selectors'; +import Connect from 'components/Connect/Connect'; import ClusterContext from 'components/contexts/ClusterContext'; +import BrokersContainer from 'components/Brokers/BrokersContainer'; +import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer'; const Cluster: React.FC = () => { const { clusterName } = useParams<{ clusterName: string }>(); const isReadOnly = useSelector(getClustersReadonlyStatus(clusterName)); + const features = useSelector(getClustersFeatures(clusterName)); + + const hasKafkaConnectConfigured = features.includes( + ClusterFeaturesEnum.KAFKA_CONNECT + ); + const hasSchemaRegistryConfigured = features.includes( + ClusterFeaturesEnum.SCHEMA_REGISTRY + ); + + const contextValue = React.useMemo( + () => ({ + isReadOnly, + hasKafkaConnectConfigured, + hasSchemaRegistryConfigured, + }), + [features] + ); + return ( - + - + - + {hasSchemaRegistryConfigured && ( + + )} + {hasKafkaConnectConfigured && ( + + )} 'mock-Topics'); +jest.mock('components/Schemas/Schemas', () => 'mock-Schemas'); +jest.mock('components/Connect/Connect', () => 'mock-Connect'); +jest.mock('components/Brokers/BrokersContainer', () => 'mock-Brokers'); +jest.mock( + 'components/ConsumerGroups/ConsumersGroupsContainer', + () => 'mock-ConsumerGroups' +); + +describe('Cluster', () => { + const setupComponent = (pathname: string) => ( + + + + + + + + ); + it('renders Brokers', () => { + const wrapper = mount(setupComponent('/ui/clusters/secondLocal/brokers')); + expect(wrapper.exists('mock-Brokers')).toBeTruthy(); + }); + it('renders Topics', () => { + const wrapper = mount(setupComponent('/ui/clusters/secondLocal/topics')); + expect(wrapper.exists('mock-Topics')).toBeTruthy(); + }); + it('renders ConsumerGroups', () => { + const wrapper = mount( + setupComponent('/ui/clusters/secondLocal/consumer-groups') + ); + expect(wrapper.exists('mock-ConsumerGroups')).toBeTruthy(); + }); + + describe('configured features', () => { + it('does not render Schemas if SCHEMA_REGISTRY is not configured', () => { + const wrapper = mount(setupComponent('/ui/clusters/secondLocal/schemas')); + expect(wrapper.exists('mock-Schemas')).toBeFalsy(); + }); + it('renders Schemas if SCHEMA_REGISTRY is configured', () => { + store.dispatch( + fetchClusterListAction.success([ + { + ...onlineClusterPayload, + features: [ClusterFeaturesEnum.SCHEMA_REGISTRY], + }, + ]) + ); + const wrapper = mount(setupComponent('/ui/clusters/secondLocal/schemas')); + expect(wrapper.exists('mock-Schemas')).toBeTruthy(); + }); + it('does not render Connect if KAFKA_CONNECT is not configured', () => { + const wrapper = mount( + setupComponent('/ui/clusters/secondLocal/connectors') + ); + expect(wrapper.exists('mock-Connect')).toBeFalsy(); + }); + it('renders Schemas if KAFKA_CONNECT is configured', async () => { + await store.dispatch( + fetchClusterListAction.success([ + { + ...onlineClusterPayload, + features: [ClusterFeaturesEnum.KAFKA_CONNECT], + }, + ]) + ); + const wrapper = mount( + setupComponent('/ui/clusters/secondLocal/connectors') + ); + expect(wrapper.exists('mock-Connect')).toBeTruthy(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Connect/Connect.tsx b/kafka-ui-react-app/src/components/Connect/Connect.tsx new file mode 100644 index 0000000000..b6a941d618 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/Connect.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Switch, Route } from 'react-router-dom'; +import { clusterConnectorsPath } from 'lib/paths'; +import ListContainer from './List/ListContainer'; + +const Connect: React.FC = () => ( + + + +); + +export default Connect; diff --git a/kafka-ui-react-app/src/components/Connect/List/List.tsx b/kafka-ui-react-app/src/components/Connect/List/List.tsx new file mode 100644 index 0000000000..7a1596e73e --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/List.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Connect, Connector } from 'generated-sources'; +import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; +import ClusterContext from 'components/contexts/ClusterContext'; +import { useParams } from 'react-router-dom'; +import { ClusterName } from 'redux/interfaces'; +import Indicator from 'components/common/Dashboard/Indicator'; +import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper'; +import PageLoader from 'components/common/PageLoader/PageLoader'; + +export interface ListProps { + areConnectsFetching: boolean; + areConnectorsFetching: boolean; + connectors: Connector[]; + connects: Connect[]; + fetchConnects(clusterName: ClusterName): void; +} + +const List: React.FC = ({ + connectors, + connects, + areConnectsFetching, + areConnectorsFetching, + fetchConnects, +}) => { + const { isReadOnly } = React.useContext(ClusterContext); + const { clusterName } = useParams<{ clusterName: string }>(); + + React.useEffect(() => { + fetchConnects(clusterName); + }, [fetchConnects, clusterName]); + + return ( +
+ All Connectors +
+ Kafka Connect section is under construction. +
+ + + {connects.length} + + + {!isReadOnly && ( +
+ +
+ )} +
+ {areConnectorsFetching ? ( + + ) : ( +
+ + + + + + + + + + + + + + + {connectors.length === 0 && ( + + + + )} + +
NameConnectTypePluginTopicsStatusTasks
No connectors found
+
+ )} +
+ ); +}; + +export default List; diff --git a/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts b/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts new file mode 100644 index 0000000000..27ffbcf7a9 --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/ListContainer.ts @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { RootState } from 'redux/interfaces'; +import { + fetchConnects, + fetchConnectors, +} from 'redux/actions/thunks/connectors'; +import { + getConnects, + getConnectors, + getAreConnectsFetching, + getAreConnectorsFetching, +} from 'redux/reducers/connect/selectors'; +import List from './List'; + +const mapStateToProps = (state: RootState) => ({ + areConnectsFetching: getAreConnectsFetching(state), + areConnectorsFetching: getAreConnectorsFetching(state), + + connects: getConnects(state), + connectors: getConnectors(state), +}); + +const mapDispatchToProps = { + fetchConnects, + fetchConnectors, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(List); diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx new file mode 100644 index 0000000000..5acebb404e --- /dev/null +++ b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import { StaticRouter } from 'react-router-dom'; +import configureStore from 'redux/store/configureStore'; +import ClusterContext, { + ContextProps, + initialValue, +} from 'components/contexts/ClusterContext'; + +import ListContainer from '../ListContainer'; +import List, { ListProps } from '../List'; + +const store = configureStore(); + +describe('Connectors List', () => { + describe('Container', () => { + it('renders view with initial state of storage', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.exists(List)).toBeTruthy(); + }); + }); + + describe('View', () => { + const fetchConnects = jest.fn(); + const setupComponent = ( + props: Partial = {}, + contextValue: ContextProps = initialValue + ) => ( + + + + + + ); + + it('renders PageLoader', () => { + const wrapper = mount(setupComponent({ areConnectorsFetching: true })); + expect(wrapper.exists('PageLoader')).toBeTruthy(); + expect(wrapper.exists('table')).toBeFalsy(); + }); + + it('renders table', () => { + const wrapper = mount(setupComponent({ areConnectorsFetching: false })); + expect(wrapper.exists('PageLoader')).toBeFalsy(); + expect(wrapper.exists('table')).toBeTruthy(); + }); + + it('handles fetchConnects', () => { + mount(setupComponent()); + expect(fetchConnects).toHaveBeenCalledTimes(1); + }); + + it('renders actions if cluster is not readonly', () => { + const wrapper = mount( + setupComponent({}, { ...initialValue, isReadOnly: false }) + ); + expect( + wrapper.exists('.level-item.level-right > button.is-primary') + ).toBeTruthy(); + }); + + describe('readonly cluster', () => { + it('does not render actions if cluster is readonly', () => { + const wrapper = mount( + setupComponent({}, { ...initialValue, isReadOnly: true }) + ); + expect( + wrapper.exists('.level-item.level-right > button.is-primary') + ).toBeFalsy(); + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx index 311a303dce..642f2b163c 100644 --- a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx +++ b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx @@ -1,101 +1,95 @@ -import React, { CSSProperties } from 'react'; +import React from 'react'; +import { Cluster, ClusterFeaturesEnum } from 'generated-sources'; import { NavLink } from 'react-router-dom'; import { clusterBrokersPath, clusterTopicsPath, clusterConsumerGroupsPath, clusterSchemasPath, + clusterConnectorsPath, } from 'lib/paths'; -import { Cluster, ServerStatus } from 'generated-sources'; +import DefaultClusterIcon from './DefaultClusterIcon'; +import ClusterStatusIcon from './ClusterStatusIcon'; interface Props { cluster: Cluster; } -const DefaultIcon: React.FC = () => { - const style: CSSProperties = { - width: '.6rem', - left: '-8px', - top: '-4px', - position: 'relative', - }; - +const ClusterMenu: React.FC = ({ + cluster: { name, status, defaultCluster, features }, +}) => { + const hasFeatureConfigured = React.useCallback( + (key) => features?.includes(key), + [features] + ); return ( - - - +
    +
  • + + {defaultCluster && } + {name} + + +
      +
    • + + Brokers + +
    • +
    • + + Topics + +
    • +
    • + + Consumers + +
    • + + {hasFeatureConfigured(ClusterFeaturesEnum.SCHEMA_REGISTRY) && ( +
    • + + Schema Registry + +
    • + )} + {hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_CONNECT) && ( +
    • + + Kafka Connect + +
    • + )} +
    +
  • +
); }; -const StatusIcon: React.FC = ({ cluster }) => { - const style: CSSProperties = { - width: '10px', - height: '10px', - borderRadius: '5px', - marginLeft: '7px', - padding: 0, - }; - - return ( - - ); -}; - -const ClusterMenu: React.FC = ({ cluster }) => ( -
    -
  • - - {cluster.defaultCluster && } - {cluster.name} - - -
      - - Brokers - - - Topics - - - Consumers - - - Schema Registry - -
    -
  • -
-); - export default ClusterMenu; diff --git a/kafka-ui-react-app/src/components/Nav/ClusterStatusIcon.tsx b/kafka-ui-react-app/src/components/Nav/ClusterStatusIcon.tsx new file mode 100644 index 0000000000..3eb84d9435 --- /dev/null +++ b/kafka-ui-react-app/src/components/Nav/ClusterStatusIcon.tsx @@ -0,0 +1,28 @@ +import { ServerStatus } from 'generated-sources'; +import React, { CSSProperties } from 'react'; + +interface Props { + status: ServerStatus; +} + +const ClusterStatusIcon: React.FC = ({ status }) => { + const style: CSSProperties = { + width: '10px', + height: '10px', + borderRadius: '5px', + marginLeft: '7px', + padding: 0, + }; + + return ( + + ); +}; + +export default ClusterStatusIcon; diff --git a/kafka-ui-react-app/src/components/Nav/DefaultClusterIcon.tsx b/kafka-ui-react-app/src/components/Nav/DefaultClusterIcon.tsx new file mode 100644 index 0000000000..d7e6594e3e --- /dev/null +++ b/kafka-ui-react-app/src/components/Nav/DefaultClusterIcon.tsx @@ -0,0 +1,22 @@ +import React, { CSSProperties } from 'react'; + +const DefaultClusterIcon: React.FC = () => { + const style: CSSProperties = { + width: '.6rem', + left: '-8px', + top: '-4px', + position: 'relative', + }; + + return ( + + + + ); +}; + +export default DefaultClusterIcon; diff --git a/kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx b/kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx new file mode 100644 index 0000000000..d5317656e3 --- /dev/null +++ b/kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { StaticRouter } from 'react-router'; +import { Cluster, ClusterFeaturesEnum } from 'generated-sources'; +import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures'; +import ClusterMenu from '../ClusterMenu'; + +describe('ClusterMenu', () => { + const setupComponent = (cluster: Cluster) => ( + + + + ); + + it('renders cluster menu without Kafka Connect & Schema Registry', () => { + const wrapper = mount(setupComponent(onlineClusterPayload)); + expect(wrapper.find('ul.menu-list > li > NavLink').text()).toEqual( + onlineClusterPayload.name + ); + + expect(wrapper.find('ul.menu-list ul > li').length).toEqual(3); + }); + + it('renders cluster menu with all enabled features', () => { + const wrapper = mount( + setupComponent({ + ...onlineClusterPayload, + features: [ + ClusterFeaturesEnum.KAFKA_CONNECT, + ClusterFeaturesEnum.SCHEMA_REGISTRY, + ], + }) + ); + expect(wrapper.find('ul.menu-list ul > li').length).toEqual(5); + }); +}); diff --git a/kafka-ui-react-app/src/components/Nav/__tests__/ClusterStatusIcon.spec.tsx b/kafka-ui-react-app/src/components/Nav/__tests__/ClusterStatusIcon.spec.tsx new file mode 100644 index 0000000000..773bf61372 --- /dev/null +++ b/kafka-ui-react-app/src/components/Nav/__tests__/ClusterStatusIcon.spec.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { ServerStatus } from 'generated-sources'; +import ClusterStatusIcon from '../ClusterStatusIcon'; + +describe('ClusterStatusIcon', () => { + it('matches snapshot', () => { + const wrapper = mount(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders online icon', () => { + const wrapper = mount(); + expect(wrapper.exists('.is-primary')).toBeTruthy(); + expect(wrapper.exists('.is-danger')).toBeFalsy(); + }); + it('renders offline icon', () => { + const wrapper = mount(); + expect(wrapper.exists('.is-danger')).toBeTruthy(); + expect(wrapper.exists('.is-primary')).toBeFalsy(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Nav/__tests__/__snapshots__/ClusterStatusIcon.spec.tsx.snap b/kafka-ui-react-app/src/components/Nav/__tests__/__snapshots__/ClusterStatusIcon.spec.tsx.snap new file mode 100644 index 0000000000..1570c06517 --- /dev/null +++ b/kafka-ui-react-app/src/components/Nav/__tests__/__snapshots__/ClusterStatusIcon.spec.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClusterStatusIcon matches snapshot 1`] = ` + + + +`; diff --git a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx index 312bd16fa4..498146a12b 100644 --- a/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx +++ b/kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx @@ -110,7 +110,13 @@ describe('Details', () => { expect( mount( - + {setupWrapper({ versions })} 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 3e3aa07f03..bcb7ad212a 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 @@ -91,7 +91,13 @@ describe('List', () => { describe('with readonly cluster', () => { const wrapper = mount( - + {setupWrapper({ schemas: [] })} diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx index dda1242413..faf33ddcd3 100644 --- a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx @@ -9,7 +9,13 @@ describe('List', () => { it('does not render the Add a Topic button', () => { const component = mount( - + { it('renders the Add a Topic button', () => { const component = mount( - + = ({ label, title, className, children }) => { +const Indicator: React.FC = ({ + label, + title, + fetching, + className, + children, +}) => { return (

{label}

-

{children}

+

+ {fetching ? ( + + + + ) : ( + children + )} +

); diff --git a/kafka-ui-react-app/src/components/common/Dashboard/__tests__/__snapshots__/Indicator.spec.tsx.snap b/kafka-ui-react-app/src/components/common/Dashboard/__tests__/__snapshots__/Indicator.spec.tsx.snap index 7cabd55e4f..1b9045d56e 100644 --- a/kafka-ui-react-app/src/components/common/Dashboard/__tests__/__snapshots__/Indicator.spec.tsx.snap +++ b/kafka-ui-react-app/src/components/common/Dashboard/__tests__/__snapshots__/Indicator.spec.tsx.snap @@ -17,7 +17,7 @@ exports[`Indicator matches the snapshot 1`] = ` label

Child

diff --git a/kafka-ui-react-app/src/components/contexts/ClusterContext.ts b/kafka-ui-react-app/src/components/contexts/ClusterContext.ts index 40e43eb0ce..5ad6fc1f70 100644 --- a/kafka-ui-react-app/src/components/contexts/ClusterContext.ts +++ b/kafka-ui-react-app/src/components/contexts/ClusterContext.ts @@ -1,7 +1,15 @@ import React from 'react'; -const initialValue: { isReadOnly: boolean } = { +export interface ContextProps { + isReadOnly: boolean; + hasKafkaConnectConfigured: boolean; + hasSchemaRegistryConfigured: boolean; +} + +export const initialValue: ContextProps = { isReadOnly: false, + hasKafkaConnectConfigured: false, + hasSchemaRegistryConfigured: false, }; const ClusterContext = React.createContext(initialValue); diff --git a/kafka-ui-react-app/src/lib/__tests__/paths.spec.ts b/kafka-ui-react-app/src/lib/__tests__/paths.spec.ts index f823810b13..f1118960c7 100644 --- a/kafka-ui-react-app/src/lib/__tests__/paths.spec.ts +++ b/kafka-ui-react-app/src/lib/__tests__/paths.spec.ts @@ -66,4 +66,10 @@ describe('Paths', () => { '/ui/clusters/local/topics/topic123/edit' ); }); + + it('clusterConnectorsPath', () => { + expect(paths.clusterConnectorsPath('local')).toEqual( + '/ui/clusters/local/connectors' + ); + }); }); diff --git a/kafka-ui-react-app/src/lib/paths.ts b/kafka-ui-react-app/src/lib/paths.ts index d615ab069d..e05e880612 100644 --- a/kafka-ui-react-app/src/lib/paths.ts +++ b/kafka-ui-react-app/src/lib/paths.ts @@ -45,3 +45,7 @@ export const clusterTopicsTopicEditPath = ( clusterName: ClusterName, topicName: TopicName ) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`; + +// Kafka Connect +export const clusterConnectorsPath = (clusterName: ClusterName) => + `${clusterPath(clusterName)}/connectors`; diff --git a/kafka-ui-react-app/src/redux/actions/__test__/thunks/connectors.spec.ts b/kafka-ui-react-app/src/redux/actions/__test__/thunks/connectors.spec.ts new file mode 100644 index 0000000000..2cd400056c --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/__test__/thunks/connectors.spec.ts @@ -0,0 +1,49 @@ +import fetchMock from 'fetch-mock-jest'; +import * as actions from 'redux/actions/actions'; +import * as thunks from 'redux/actions/thunks'; +import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; + +const store = mockStoreCreator; +const clusterName = 'local'; + +describe('Thunks', () => { + afterEach(() => { + fetchMock.restore(); + store.clearActions(); + }); + + describe('fetchConnects', () => { + it('creates GET_CONNECTS__SUCCESS when fetching connects', async () => { + fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, [ + { name: 'first', address: 'localhost' }, + ]); + await store.dispatch(thunks.fetchConnects(clusterName)); + expect(store.getActions()).toEqual([ + actions.fetchConnectsAction.request(), + actions.fetchConnectsAction.success({ + ...store.getState().connect, + connects: [{ name: 'first', address: 'localhost' }], + }), + ]); + }); + + it('creates GET_CONNECTS__FAILURE', async () => { + fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, 404); + await store.dispatch(thunks.fetchConnects(clusterName)); + expect(store.getActions()).toEqual([ + actions.fetchConnectsAction.request(), + actions.fetchConnectsAction.failure({ + alert: { + subject: 'connects', + title: `Kafka Connect`, + response: { + status: 404, + statusText: 'Not Found', + body: undefined, + }, + }, + }), + ]); + }); + }); +}); 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 7515cae1f7..a20bb25e0d 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 @@ -1,25 +1,11 @@ -import configureMockStore, { - MockStoreCreator, - MockStoreEnhanced, -} from 'redux-mock-store'; -import thunk, { ThunkDispatch } from 'redux-thunk'; import fetchMock from 'fetch-mock-jest'; -import { Middleware } from 'redux'; -import { RootState, Action } from 'redux/interfaces'; import * as actions from 'redux/actions/actions'; import * as thunks from 'redux/actions/thunks'; import * as schemaFixtures from 'redux/reducers/schemas/__test__/fixtures'; +import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; import * as fixtures from '../fixtures'; -const middlewares: Array = [thunk]; -type DispatchExts = ThunkDispatch; - -const mockStoreCreator: MockStoreCreator< - RootState, - DispatchExts -> = configureMockStore(middlewares); - -const store: MockStoreEnhanced = mockStoreCreator(); +const store = mockStoreCreator; const clusterName = 'local'; const subject = 'test'; diff --git a/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts b/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts index b69a7caaba..3d27a9c30e 100644 --- a/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts +++ b/kafka-ui-react-app/src/redux/actions/__test__/thunks/topics.spec.ts @@ -1,23 +1,9 @@ -import configureMockStore, { - MockStoreCreator, - MockStoreEnhanced, -} from 'redux-mock-store'; -import thunk, { ThunkDispatch } from 'redux-thunk'; import fetchMock from 'fetch-mock-jest'; -import { Middleware } from 'redux'; -import { RootState, Action } from 'redux/interfaces'; import * as actions from 'redux/actions/actions'; import * as thunks from 'redux/actions/thunks'; +import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator'; -const middlewares: Array = [thunk]; -type DispatchExts = ThunkDispatch; - -const mockStoreCreator: MockStoreCreator< - RootState, - DispatchExts -> = configureMockStore(middlewares); - -const store: MockStoreEnhanced = mockStoreCreator(); +const store = mockStoreCreator; const clusterName = 'local'; const topicName = 'localTopic'; diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts index 64d3967aca..ea17c103f8 100644 --- a/kafka-ui-react-app/src/redux/actions/actions.ts +++ b/kafka-ui-react-app/src/redux/actions/actions.ts @@ -4,8 +4,8 @@ import { FailurePayload, TopicName, TopicsState, + ConnectState, } from 'redux/interfaces'; - import { Cluster, ClusterStats, @@ -125,3 +125,21 @@ export const createSchemaAction = createAsyncAction( )(); export const dismissAlert = createAction('DISMISS_ALERT')(); + +export const fetchConnectsAction = createAsyncAction( + 'GET_CONNECTS__REQUEST', + 'GET_CONNECTS__SUCCESS', + 'GET_CONNECTS__FAILURE' +)(); + +export const fetchConnectorsAction = createAsyncAction( + 'GET_CONNECTORS__REQUEST', + 'GET_CONNECTORS__SUCCESS', + 'GET_CONNECTORS__FAILURE' +)(); + +export const fetchConnectorAction = createAsyncAction( + 'GET_CONNECTOR__REQUEST', + 'GET_CONNECTOR__SUCCESS', + 'GET_CONNECTOR__FAILURE' +)(); diff --git a/kafka-ui-react-app/src/redux/actions/thunks/connectors.ts b/kafka-ui-react-app/src/redux/actions/thunks/connectors.ts new file mode 100644 index 0000000000..645504a1d5 --- /dev/null +++ b/kafka-ui-react-app/src/redux/actions/thunks/connectors.ts @@ -0,0 +1,94 @@ +import { KafkaConnectApi, Configuration } from 'generated-sources'; +import { BASE_PARAMS } from 'lib/constants'; + +import { + ClusterName, + FailurePayload, + PromiseThunkResult, +} from 'redux/interfaces'; +import * as actions from 'redux/actions'; +import { getResponse } from 'lib/errorHandling'; + +const apiClientConf = new Configuration(BASE_PARAMS); +export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf); + +export const fetchConnects = ( + clusterName: ClusterName +): PromiseThunkResult => async (dispatch, getState) => { + dispatch(actions.fetchConnectsAction.request()); + try { + const connects = await kafkaConnectApiClient.getConnects({ clusterName }); + const state = getState().connect; + dispatch(actions.fetchConnectsAction.success({ ...state, connects })); + } catch (error) { + const response = await getResponse(error); + const alert: FailurePayload = { + subject: 'connects', + title: `Kafka Connect`, + response, + }; + dispatch(actions.fetchConnectsAction.failure({ alert })); + } +}; + +export const fetchConnectors = ( + clusterName: ClusterName, + connectName: string +): PromiseThunkResult => async (dispatch, getState) => { + dispatch(actions.fetchConnectorsAction.request()); + try { + const connectorNames = await kafkaConnectApiClient.getConnectors({ + clusterName, + connectName, + }); + const connectors = await Promise.all( + connectorNames.map((connectorName) => + kafkaConnectApiClient.getConnector({ + clusterName, + connectName, + connectorName, + }) + ) + ); + const state = getState().connect; + dispatch(actions.fetchConnectorsAction.success({ ...state, connectors })); + } catch (error) { + const response = await getResponse(error); + const alert: FailurePayload = { + subject: ['connect', connectName, 'connectors'].join('-'), + title: `Kafka Connect ${connectName}. Connectors`, + response, + }; + dispatch(actions.fetchConnectorsAction.failure({ alert })); + } +}; + +export const fetchConnector = ( + clusterName: ClusterName, + connectName: string, + connectorName: string +): PromiseThunkResult => async (dispatch, getState) => { + dispatch(actions.fetchConnectorAction.request()); + try { + const connector = await kafkaConnectApiClient.getConnector({ + clusterName, + connectName, + connectorName, + }); + const state = getState().connect; + const newState = { + ...state, + connectors: [...state.connectors, connector], + }; + + dispatch(actions.fetchConnectorAction.success(newState)); + } catch (error) { + const response = await getResponse(error); + const alert: FailurePayload = { + subject: ['connect', connectName, 'connectors', connectorName].join('-'), + title: `Kafka Connect ${connectName}. Connector ${connectorName}`, + response, + }; + dispatch(actions.fetchConnectorAction.failure({ alert })); + } +}; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/index.ts b/kafka-ui-react-app/src/redux/actions/thunks/index.ts index 2eadadd730..41b012442f 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks/index.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks/index.ts @@ -3,3 +3,4 @@ export * from './clusters'; export * from './consumerGroups'; export * from './schemas'; export * from './topics'; +export * from './connectors'; 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 9319f3219b..0fde97af2f 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks/schemas.ts @@ -61,8 +61,7 @@ export const createSchema = ( } catch (error) { const response = await getResponse(error); const alert: FailurePayload = { - subject: 'schema', - subjectId: newSchemaSubject.subject, + subject: ['schema', newSchemaSubject.subject].join('-'), title: `Schema ${newSchemaSubject.subject}`, response, }; diff --git a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts index baa0395cdd..58d88bc60f 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks/topics.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks/topics.ts @@ -232,8 +232,7 @@ export const createTopic = ( } catch (error) { const response = await getResponse(error); const alert: FailurePayload = { - subjectId: form.name, - subject: 'schema', + subject: ['schema', form.name].join('-'), title: `Schema ${form.name}`, response, }; diff --git a/kafka-ui-react-app/src/redux/interfaces/alerts.ts b/kafka-ui-react-app/src/redux/interfaces/alerts.ts index 9b6f53d52a..80f0314752 100644 --- a/kafka-ui-react-app/src/redux/interfaces/alerts.ts +++ b/kafka-ui-react-app/src/redux/interfaces/alerts.ts @@ -4,14 +4,13 @@ import React from 'react'; export interface ServerResponse { status: number; statusText: string; - body: ErrorResponse; + body?: ErrorResponse; } export interface FailurePayload { title: string; message?: string; subject: string; - subjectId?: string | number; response?: ServerResponse; } diff --git a/kafka-ui-react-app/src/redux/interfaces/connect.ts b/kafka-ui-react-app/src/redux/interfaces/connect.ts new file mode 100644 index 0000000000..f30801cdec --- /dev/null +++ b/kafka-ui-react-app/src/redux/interfaces/connect.ts @@ -0,0 +1,6 @@ +import { Connect, Connector } from 'generated-sources'; + +export interface ConnectState { + connects: Connect[]; + connectors: Connector[]; +} diff --git a/kafka-ui-react-app/src/redux/interfaces/index.ts b/kafka-ui-react-app/src/redux/interfaces/index.ts index e53d5a1021..8427d3cc3f 100644 --- a/kafka-ui-react-app/src/redux/interfaces/index.ts +++ b/kafka-ui-react-app/src/redux/interfaces/index.ts @@ -8,6 +8,7 @@ import { LoaderState } from './loader'; import { ConsumerGroupsState } from './consumerGroup'; import { SchemasState } from './schema'; import { AlertsState } from './alerts'; +import { ConnectState } from './connect'; export * from './topic'; export * from './cluster'; @@ -16,6 +17,7 @@ export * from './consumerGroup'; export * from './schema'; export * from './loader'; export * from './alerts'; +export * from './connect'; export interface RootState { topics: TopicsState; @@ -23,6 +25,7 @@ export interface RootState { brokers: BrokersState; consumerGroups: ConsumerGroupsState; schemas: SchemasState; + connect: ConnectState; loader: LoaderState; alerts: AlertsState; } diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/fixtures.ts index 295e0c3e30..56161dc15e 100644 --- a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/fixtures.ts +++ b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/fixtures.ts @@ -1,10 +1,10 @@ -export const failurePayloadWithoutId = { +export const failurePayload1 = { title: 'title', message: 'message', - subject: 'topic', + subject: 'topic-1', }; -export const failurePayloadWithId = { - ...failurePayloadWithoutId, - subjectId: '12345', +export const failurePayload2 = { + ...failurePayload1, + subject: 'topic-2', }; diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts index ae9c267e25..d3fc84bd89 100644 --- a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts @@ -1,6 +1,6 @@ import { dismissAlert, createTopicAction } from 'redux/actions'; import reducer from 'redux/reducers/alerts/reducer'; -import { failurePayloadWithId, failurePayloadWithoutId } from './fixtures'; +import { failurePayload1, failurePayload2 } from './fixtures'; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -12,38 +12,18 @@ describe('Clusters reducer', () => { expect(reducer(undefined, createTopicAction.failure({}))).toEqual({}); }); - it('creates error alert with subjectId', () => { + it('creates error alert', () => { expect( reducer( undefined, createTopicAction.failure({ - alert: failurePayloadWithId, + alert: failurePayload2, }) ) ).toEqual({ - 'alert-topic12345': { + 'alert-topic-2': { createdAt: 1234567890, - id: 'alert-topic12345', - message: 'message', - response: undefined, - title: 'title', - type: 'error', - }, - }); - }); - - it('creates error alert without subjectId', () => { - expect( - reducer( - undefined, - createTopicAction.failure({ - alert: failurePayloadWithoutId, - }) - ) - ).toEqual({ - 'alert-topic': { - createdAt: 1234567890, - id: 'alert-topic', + id: 'alert-topic-2', message: 'message', response: undefined, title: 'title', @@ -56,23 +36,23 @@ describe('Clusters reducer', () => { const state = reducer( undefined, createTopicAction.failure({ - alert: failurePayloadWithoutId, + alert: failurePayload1, }) ); - expect(reducer(state, dismissAlert('alert-topic'))).toEqual({}); + expect(reducer(state, dismissAlert('alert-topic-1'))).toEqual({}); }); it('does not remove alert if id is wrong', () => { const state = reducer( undefined, createTopicAction.failure({ - alert: failurePayloadWithoutId, + alert: failurePayload1, }) ); expect(reducer(state, dismissAlert('wrong-id'))).toEqual({ - 'alert-topic': { + 'alert-topic-1': { createdAt: 1234567890, - id: 'alert-topic', + id: 'alert-topic-1', message: 'message', response: undefined, title: 'title', diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/selectors.spec.ts b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/selectors.spec.ts index 581287cf9e..bd8a77d866 100644 --- a/kafka-ui-react-app/src/redux/reducers/alerts/__test__/selectors.spec.ts +++ b/kafka-ui-react-app/src/redux/reducers/alerts/__test__/selectors.spec.ts @@ -1,7 +1,7 @@ import configureStore from 'redux/store/configureStore'; import { createTopicAction } from 'redux/actions'; import * as selectors from '../selectors'; -import { failurePayloadWithId, failurePayloadWithoutId } from './fixtures'; +import { failurePayload1, failurePayload2 } from './fixtures'; const store = configureStore(); @@ -14,12 +14,8 @@ describe('Alerts selectors', () => { describe('state', () => { beforeAll(() => { - store.dispatch( - createTopicAction.failure({ alert: failurePayloadWithoutId }) - ); - store.dispatch( - createTopicAction.failure({ alert: failurePayloadWithId }) - ); + store.dispatch(createTopicAction.failure({ alert: failurePayload1 })); + store.dispatch(createTopicAction.failure({ alert: failurePayload2 })); }); it('returns fetch status', () => { diff --git a/kafka-ui-react-app/src/redux/reducers/alerts/utils.ts b/kafka-ui-react-app/src/redux/reducers/alerts/utils.ts index d30e22a8fa..c8573c79db 100644 --- a/kafka-ui-react-app/src/redux/reducers/alerts/utils.ts +++ b/kafka-ui-react-app/src/redux/reducers/alerts/utils.ts @@ -8,15 +8,9 @@ export const addError = (state: AlertsState, action: Action) => { 'alert' in action.payload && action.payload.alert !== undefined ) { - const { - subject, - subjectId, - title, - message, - response, - } = action.payload.alert; + const { subject, title, message, response } = action.payload.alert; - const id = `alert-${subject}${subjectId || ''}`; + const id = `alert-${subject}`; return { ...state, diff --git a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts index 059c7c96a5..c1d048658f 100644 --- a/kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts +++ b/kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts @@ -9,6 +9,7 @@ export const onlineClusterPayload: Cluster = { topicCount: 3, bytesInPerSec: 1.55, bytesOutPerSec: 9.314, + features: [], }; export const offlineClusterPayload: Cluster = { name: 'local', @@ -19,6 +20,7 @@ export const offlineClusterPayload: Cluster = { topicCount: 2, bytesInPerSec: 3.42, bytesOutPerSec: 4.14, + features: [], }; export const clustersPayload: Cluster[] = [ diff --git a/kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts b/kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts index a8b06b0f17..d1cdde641c 100644 --- a/kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/clusters/selectors.ts @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import { RootState } from 'redux/interfaces'; import { createFetchingSelector } from 'redux/reducers/loader/selectors'; -import { Cluster, ServerStatus } from 'generated-sources'; +import { Cluster, ClusterFeaturesEnum, ServerStatus } from 'generated-sources'; const clustersState = ({ clusters }: RootState): Cluster[] => clusters; @@ -31,3 +31,10 @@ export const getClustersReadonlyStatus = (clusterName: string) => (clusters): boolean => clusters.find(({ name }) => name === clusterName)?.readOnly || false ); + +export const getClustersFeatures = (clusterName: string) => + createSelector( + getClusterList, + (clusters): ClusterFeaturesEnum[] => + clusters.find(({ name }) => name === clusterName)?.features || [] + ); diff --git a/kafka-ui-react-app/src/redux/reducers/connect/__tests__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/connect/__tests__/reducer.spec.ts new file mode 100644 index 0000000000..dc7c546773 --- /dev/null +++ b/kafka-ui-react-app/src/redux/reducers/connect/__tests__/reducer.spec.ts @@ -0,0 +1,24 @@ +import { + fetchConnectorsAction, + fetchConnectorAction, + fetchConnectsAction, +} from 'redux/actions'; +import reducer, { initialState } from 'redux/reducers/connect/reducer'; + +describe('Clusters reducer', () => { + it('reacts on GET_CONNECTS__SUCCESS and returns payload', () => { + expect( + reducer(undefined, fetchConnectsAction.success(initialState)) + ).toEqual(initialState); + }); + it('reacts on GET_CONNECTORS__SUCCESS and returns payload', () => { + expect( + reducer(undefined, fetchConnectorsAction.success(initialState)) + ).toEqual(initialState); + }); + it('reacts on GET_CONNECTOR__SUCCESS and returns payload', () => { + expect( + reducer(undefined, fetchConnectorAction.success(initialState)) + ).toEqual(initialState); + }); +}); diff --git a/kafka-ui-react-app/src/redux/reducers/connect/reducer.ts b/kafka-ui-react-app/src/redux/reducers/connect/reducer.ts new file mode 100644 index 0000000000..bd179681a6 --- /dev/null +++ b/kafka-ui-react-app/src/redux/reducers/connect/reducer.ts @@ -0,0 +1,22 @@ +import { getType } from 'typesafe-actions'; +import * as actions from 'redux/actions'; +import { ConnectState } from 'redux/interfaces/connect'; +import { Action } from 'redux/interfaces'; + +export const initialState: ConnectState = { + connects: [], + connectors: [], +}; + +const reducer = (state = initialState, action: Action): ConnectState => { + switch (action.type) { + case getType(actions.fetchConnectsAction.success): + case getType(actions.fetchConnectorAction.success): + case getType(actions.fetchConnectorsAction.success): + return action.payload; + default: + return state; + } +}; + +export default reducer; diff --git a/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts b/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts new file mode 100644 index 0000000000..32002474a3 --- /dev/null +++ b/kafka-ui-react-app/src/redux/reducers/connect/selectors.ts @@ -0,0 +1,35 @@ +import { createSelector } from 'reselect'; +import { ConnectState, RootState } from 'redux/interfaces'; +import { createFetchingSelector } from 'redux/reducers/loader/selectors'; + +const connectState = ({ connect }: RootState): ConnectState => connect; + +const getConnectorsFetchingStatus = createFetchingSelector('GET_CONNECTORS'); +export const getAreConnectorsFetching = createSelector( + getConnectorsFetchingStatus, + (status) => status === 'fetching' +); +export const getAreConnectorsFetched = createSelector( + getConnectorsFetchingStatus, + (status) => status === 'fetched' +); + +const getConnectsFetchingStatus = createFetchingSelector('GET_CONNECTS'); +export const getAreConnectsFetching = createSelector( + getConnectsFetchingStatus, + (status) => status === 'fetching' || status === 'notFetched' +); +export const getAreConnectsFetched = createSelector( + getConnectsFetchingStatus, + (status) => status === 'fetched' +); + +export const getConnects = createSelector( + connectState, + ({ connects }) => connects +); + +export const getConnectors = createSelector( + connectState, + ({ connectors }) => connectors +); diff --git a/kafka-ui-react-app/src/redux/reducers/index.ts b/kafka-ui-react-app/src/redux/reducers/index.ts index c5505bbcb0..005a0e680a 100644 --- a/kafka-ui-react-app/src/redux/reducers/index.ts +++ b/kafka-ui-react-app/src/redux/reducers/index.ts @@ -5,6 +5,7 @@ import clusters from './clusters/reducer'; import brokers from './brokers/reducer'; import consumerGroups from './consumerGroups/reducer'; import schemas from './schemas/reducer'; +import connect from './connect/reducer'; import loader from './loader/reducer'; import alerts from './alerts/reducer'; @@ -14,6 +15,7 @@ export default combineReducers({ brokers, consumerGroups, schemas, + connect, loader, alerts, }); diff --git a/kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts b/kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts new file mode 100644 index 0000000000..373edbe3a0 --- /dev/null +++ b/kafka-ui-react-app/src/redux/store/configureStore/mockStoreCreator.ts @@ -0,0 +1,14 @@ +import configureMockStore, { MockStoreCreator } from 'redux-mock-store'; +import thunk, { ThunkDispatch } from 'redux-thunk'; +import { Middleware } from 'redux'; +import { RootState, Action } from 'redux/interfaces'; + +const middlewares: Array = [thunk]; +type DispatchExts = ThunkDispatch; + +const mockStoreCreator: MockStoreCreator< + RootState, + DispatchExts +> = configureMockStore(middlewares); + +export default mockStoreCreator();