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 ? (
+
+ ) : (
+
+
+
+
+ Name |
+ Connect |
+ Type |
+ Plugin |
+ Topics |
+ Status |
+ Tasks |
+ |
+
+
+
+ {connectors.length === 0 && (
+
+ 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();