Browse Source

[Chore] Connect. Connectors page (#382)

Oleg Shur 4 years ago
parent
commit
ab57772329
41 changed files with 789 additions and 166 deletions
  1. 1 1
      kafka-ui-react-app/sonar-project.properties
  2. 0 5
      kafka-ui-react-app/src/components/Alert/__tests__/Alert.spec.tsx
  3. 1 1
      kafka-ui-react-app/src/components/Brokers/Brokers.tsx
  4. 38 26
      kafka-ui-react-app/src/components/Connect/List/List.tsx
  5. 0 1
      kafka-ui-react-app/src/components/Connect/List/ListContainer.ts
  6. 89 0
      kafka-ui-react-app/src/components/Connect/List/ListItem.tsx
  7. 17 1
      kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx
  8. 67 0
      kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx
  9. 192 0
      kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap
  10. 20 0
      kafka-ui-react-app/src/components/Connect/StatusTag.tsx
  11. 1 1
      kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx
  12. 1 1
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx
  13. 1 1
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx
  14. 1 1
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx
  15. 1 1
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/__snapshots__/ClusterWidget.spec.tsx.snap
  16. 1 1
      kafka-ui-react-app/src/components/Nav/ClusterStatusIcon.tsx
  17. 2 2
      kafka-ui-react-app/src/components/Nav/__tests__/ClusterStatusIcon.spec.tsx
  18. 1 1
      kafka-ui-react-app/src/components/Nav/__tests__/__snapshots__/ClusterStatusIcon.spec.tsx.snap
  19. 1 1
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  20. 23 23
      kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx
  21. 7 7
      kafka-ui-react-app/src/components/Schemas/Details/__test__/__snapshots__/Details.spec.tsx.snap
  22. 3 5
      kafka-ui-react-app/src/components/Topics/List/ListItem.tsx
  23. 47 15
      kafka-ui-react-app/src/components/Topics/List/__tests__/ListItem.spec.tsx
  24. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx
  25. 1 2
      kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx
  26. 4 4
      kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx
  27. 1 1
      kafka-ui-react-app/src/components/__tests__/__snapshots__/App.spec.tsx.snap
  28. 109 4
      kafka-ui-react-app/src/redux/actions/__test__/thunks/connectors.spec.ts
  29. 6 0
      kafka-ui-react-app/src/redux/actions/actions.ts
  30. 20 28
      kafka-ui-react-app/src/redux/actions/thunks/connectors.ts
  31. 2 2
      kafka-ui-react-app/src/redux/interfaces/connect.ts
  32. 1 4
      kafka-ui-react-app/src/redux/interfaces/topic.ts
  33. 7 5
      kafka-ui-react-app/src/redux/reducers/alerts/__test__/reducer.spec.ts
  34. 1 1
      kafka-ui-react-app/src/redux/reducers/alerts/utils.ts
  35. 56 0
      kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts
  36. 0 0
      kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts
  37. 1 0
      kafka-ui-react-app/src/redux/reducers/connect/reducer.ts
  38. 43 0
      kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts
  39. 0 0
      kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts
  40. 5 0
      kafka-ui-react-app/src/setupTests.ts
  41. 16 19
      kafka-ui-react-app/src/theme/index.scss

+ 1 - 1
kafka-ui-react-app/sonar-project.properties

@@ -2,7 +2,7 @@ sonar.projectKey=provectus_kafka-ui_frontend
 sonar.organization=provectus
 
 sonar.sources=.
-sonar.exclusions="**/__test?__/**"
+sonar.exclusions="**/__test?__/**,src/setupWorker.ts,src/setupTests.ts,**/fixtures.ts"
 
 sonar.typescript.lcov.reportPaths=./coverage/lcov.info
 sonar.testExecutionReportPaths=./test-report.xml

+ 0 - 5
kafka-ui-react-app/src/components/Alert/__tests__/Alert.spec.tsx

@@ -4,11 +4,6 @@ import { Alert as AlertProps } from 'redux/interfaces';
 import * as actions from 'redux/actions/actions';
 import Alert from '../Alert';
 
-jest.mock('react-redux', () => ({
-  ...jest.requireActual('react-redux'),
-  useDispatch: () => jest.fn(),
-}));
-
 const id = 'test-id';
 const title = 'My Alert Title';
 const message = 'My Alert Message';

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

@@ -52,7 +52,7 @@ const Brokers: React.FC<Props> = ({
           {activeControllers}
         </Indicator>
         <Indicator className="is-one-third" label="Zookeeper Status">
-          <span className={cx('tag', zkOnline ? 'is-primary' : 'is-danger')}>
+          <span className={cx('tag', zkOnline ? 'is-success' : 'is-danger')}>
             {zkOnline ? 'Online' : 'Offline'}
           </span>
         </Indicator>

+ 38 - 26
kafka-ui-react-app/src/components/Connect/List/List.tsx

@@ -1,19 +1,21 @@
 import React from 'react';
-import { Connect, Connector } from 'generated-sources';
-import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
-import ClusterContext from 'components/contexts/ClusterContext';
+import { Connect, FullConnectorInfo } from 'generated-sources';
 import { useParams } from 'react-router-dom';
 import { ClusterName } from 'redux/interfaces';
+import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
+import ClusterContext from 'components/contexts/ClusterContext';
 import Indicator from 'components/common/Dashboard/Indicator';
 import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
 import PageLoader from 'components/common/PageLoader/PageLoader';
+import ListItem from './ListItem';
 
 export interface ListProps {
   areConnectsFetching: boolean;
   areConnectorsFetching: boolean;
-  connectors: Connector[];
+  connectors: FullConnectorInfo[];
   connects: Connect[];
   fetchConnects(clusterName: ClusterName): void;
+  fetchConnectors(clusterName: ClusterName): void;
 }
 
 const List: React.FC<ListProps> = ({
@@ -22,13 +24,15 @@ const List: React.FC<ListProps> = ({
   areConnectsFetching,
   areConnectorsFetching,
   fetchConnects,
+  fetchConnectors,
 }) => {
   const { isReadOnly } = React.useContext(ClusterContext);
   const { clusterName } = useParams<{ clusterName: string }>();
 
   React.useEffect(() => {
     fetchConnects(clusterName);
-  }, [fetchConnects, clusterName]);
+    fetchConnectors(clusterName);
+  }, [fetchConnects, fetchConnectors, clusterName]);
 
   return (
     <div className="section">
@@ -38,6 +42,7 @@ const List: React.FC<ListProps> = ({
       </div>
       <MetricsWrapper>
         <Indicator
+          className="level-left is-one-third"
           label="Connects"
           title="Connects"
           fetching={areConnectsFetching}
@@ -57,29 +62,36 @@ const List: React.FC<ListProps> = ({
         <PageLoader />
       ) : (
         <div className="box">
-          <div className="table-container">
-            <table className="table is-fullwidth">
-              <thead>
+          <table className="table is-fullwidth">
+            <thead>
+              <tr>
+                <th>Name</th>
+                <th>Connect</th>
+                <th>Type</th>
+                <th>Plugin</th>
+                <th>Topics</th>
+                <th>Status</th>
+                <th>Running Tasks</th>
+                <th> </th>
+              </tr>
+            </thead>
+            <tbody>
+              {connectors.length === 0 && (
                 <tr>
-                  <th>Name</th>
-                  <th>Connect</th>
-                  <th>Type</th>
-                  <th>Plugin</th>
-                  <th>Topics</th>
-                  <th>Status</th>
-                  <th>Tasks</th>
-                  <th> </th>
+                  <td colSpan={10}>No connectors found</td>
                 </tr>
-              </thead>
-              <tbody>
-                {connectors.length === 0 && (
-                  <tr>
-                    <td colSpan={10}>No connectors found</td>
-                  </tr>
-                )}
-              </tbody>
-            </table>
-          </div>
+              )}
+              {connectors.map((connector) => (
+                <ListItem
+                  key={[connector.name, connector.connect, clusterName].join(
+                    '-'
+                  )}
+                  connector={connector}
+                  clusterName={clusterName}
+                />
+              ))}
+            </tbody>
+          </table>
         </div>
       )}
     </div>

+ 0 - 1
kafka-ui-react-app/src/components/Connect/List/ListContainer.ts

@@ -15,7 +15,6 @@ import List from './List';
 const mapStateToProps = (state: RootState) => ({
   areConnectsFetching: getAreConnectsFetching(state),
   areConnectorsFetching: getAreConnectorsFetching(state),
-
   connects: getConnects(state),
   connectors: getConnectors(state),
 });

+ 89 - 0
kafka-ui-react-app/src/components/Connect/List/ListItem.tsx

@@ -0,0 +1,89 @@
+import React from 'react';
+import cx from 'classnames';
+import { FullConnectorInfo } from 'generated-sources';
+import { clusterTopicPath } from 'lib/paths';
+import { ClusterName } from 'redux/interfaces';
+import { Link } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
+import { deleteConnector } from 'redux/actions';
+import Dropdown from 'components/common/Dropdown/Dropdown';
+import DropdownDivider from 'components/common/Dropdown/DropdownDivider';
+import DropdownItem from 'components/common/Dropdown/DropdownItem';
+import StatusTag from '../StatusTag';
+
+export interface ListItemProps {
+  clusterName: ClusterName;
+  connector: FullConnectorInfo;
+}
+
+const ListItem: React.FC<ListItemProps> = ({
+  clusterName,
+  connector: {
+    name,
+    connect,
+    type,
+    connectorClass,
+    topics,
+    status,
+    tasksCount,
+    failedTasksCount,
+  },
+}) => {
+  const dispatch = useDispatch();
+
+  const handleDelete = React.useCallback(() => {
+    if (clusterName && connect && name) {
+      dispatch(deleteConnector(clusterName, connect, name));
+    }
+  }, [clusterName, connect, name]);
+
+  const runningTasks = React.useMemo(() => {
+    if (!tasksCount) return null;
+    return tasksCount - (failedTasksCount || 0);
+  }, [tasksCount, failedTasksCount]);
+
+  return (
+    <tr>
+      <td className="has-text-overflow-ellipsis">{name}</td>
+      <td>{connect}</td>
+      <td>{type}</td>
+      <td>{connectorClass}</td>
+      <td>
+        {topics?.map((t) => (
+          <Link className="mr-1" key={t} to={clusterTopicPath(clusterName, t)}>
+            {t}
+          </Link>
+        ))}
+      </td>
+      <td>{status && <StatusTag status={status} />}</td>
+      <td>
+        {runningTasks && (
+          <span
+            className={cx(
+              failedTasksCount ? 'has-text-danger' : 'has-text-success'
+            )}
+          >
+            {runningTasks} of {tasksCount}
+          </span>
+        )}
+      </td>
+      <td className="has-text-right">
+        <Dropdown
+          label={
+            <span className="icon">
+              <i className="fas fa-cog" />
+            </span>
+          }
+          right
+        >
+          <DropdownDivider />
+          <DropdownItem onClick={handleDelete}>
+            <span className="has-text-danger">Remove Connector</span>
+          </DropdownItem>
+        </Dropdown>
+      </td>
+    </tr>
+  );
+};
+
+export default ListItem;

+ 17 - 1
kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx

@@ -3,6 +3,7 @@ import { mount } from 'enzyme';
 import { Provider } from 'react-redux';
 import { StaticRouter } from 'react-router-dom';
 import configureStore from 'redux/store/configureStore';
+import { connectorsPayload } from 'redux/reducers/connect/__test__/fixtures';
 import ClusterContext, {
   ContextProps,
   initialValue,
@@ -30,6 +31,7 @@ describe('Connectors List', () => {
 
   describe('View', () => {
     const fetchConnects = jest.fn();
+    const fetchConnectors = jest.fn();
     const setupComponent = (
       props: Partial<ListProps> = {},
       contextValue: ContextProps = initialValue
@@ -42,6 +44,7 @@ describe('Connectors List', () => {
             connectors={[]}
             connects={[]}
             fetchConnects={fetchConnects}
+            fetchConnectors={fetchConnectors}
             {...props}
           />
         </ClusterContext.Provider>
@@ -60,9 +63,22 @@ describe('Connectors List', () => {
       expect(wrapper.exists('table')).toBeTruthy();
     });
 
-    it('handles fetchConnects', () => {
+    it('renders connectors list', () => {
+      const wrapper = mount(
+        setupComponent({
+          areConnectorsFetching: false,
+          connectors: connectorsPayload,
+        })
+      );
+      expect(wrapper.exists('PageLoader')).toBeFalsy();
+      expect(wrapper.exists('table')).toBeTruthy();
+      expect(wrapper.find('ListItem').length).toEqual(2);
+    });
+
+    it('handles fetchConnects and fetchConnectors', () => {
       mount(setupComponent());
       expect(fetchConnects).toHaveBeenCalledTimes(1);
+      expect(fetchConnectors).toHaveBeenCalledTimes(1);
     });
 
     it('renders actions if cluster is not readonly', () => {

+ 67 - 0
kafka-ui-react-app/src/components/Connect/List/__tests__/ListItem.spec.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+import { connectorsPayload } from 'redux/reducers/connect/__test__/fixtures';
+import configureStore from 'redux/store/configureStore';
+import ListItem, { ListItemProps } from '../ListItem';
+
+const store = configureStore();
+
+describe('Connectors ListItem', () => {
+  const connector = connectorsPayload[0];
+  const setupWrapper = (props: Partial<ListItemProps> = {}) => (
+    <Provider store={store}>
+      <BrowserRouter>
+        <table>
+          <tbody>
+            <ListItem clusterName="local" connector={connector} {...props} />
+          </tbody>
+        </table>
+      </BrowserRouter>
+    </Provider>
+  );
+
+  it('renders item', () => {
+    const wrapper = mount(setupWrapper());
+    expect(wrapper.find('td').at(6).find('.has-text-success').text()).toEqual(
+      '2 of 2'
+    );
+  });
+
+  it('renders item with failed tasks', () => {
+    const wrapper = mount(
+      setupWrapper({
+        connector: {
+          ...connector,
+          failedTasksCount: 1,
+        },
+      })
+    );
+    expect(wrapper.find('td').at(6).find('.has-text-danger').text()).toEqual(
+      '1 of 2'
+    );
+  });
+
+  it('does not render info about tasks if taksCount is undefined', () => {
+    const wrapper = mount(
+      setupWrapper({
+        connector: {
+          ...connector,
+          tasksCount: undefined,
+        },
+      })
+    );
+    expect(wrapper.find('td').at(6).text()).toEqual('');
+  });
+
+  it('handles delete', () => {
+    const wrapper = mount(setupWrapper());
+    wrapper.find('DropdownItem a').last().simulate('click');
+  });
+
+  it('matches snapshot', () => {
+    const wrapper = mount(setupWrapper());
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 192 - 0
kafka-ui-react-app/src/components/Connect/List/__tests__/__snapshots__/ListItem.spec.tsx.snap

@@ -0,0 +1,192 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Connectors ListItem matches snapshot 1`] = `
+<Provider
+  store={
+    Object {
+      "dispatch": [Function],
+      "getState": [Function],
+      "replaceReducer": [Function],
+      "subscribe": [Function],
+      Symbol(observable): [Function],
+    }
+  }
+>
+  <BrowserRouter>
+    <Router
+      history={
+        Object {
+          "action": "POP",
+          "block": [Function],
+          "createHref": [Function],
+          "go": [Function],
+          "goBack": [Function],
+          "goForward": [Function],
+          "length": 1,
+          "listen": [Function],
+          "location": Object {
+            "hash": "",
+            "pathname": "/",
+            "search": "",
+            "state": undefined,
+          },
+          "push": [Function],
+          "replace": [Function],
+        }
+      }
+    >
+      <table>
+        <tbody>
+          <ListItem
+            clusterName="local"
+            connector={
+              Object {
+                "connect": "first",
+                "connectorClass": "FileStreamSource",
+                "failedTasksCount": 0,
+                "name": "hdfs-source-connector",
+                "status": "RUNNING",
+                "tasksCount": 2,
+                "topics": Array [
+                  "test-topic",
+                ],
+                "type": "SOURCE",
+              }
+            }
+          >
+            <tr>
+              <td
+                className="has-text-overflow-ellipsis"
+              >
+                hdfs-source-connector
+              </td>
+              <td>
+                first
+              </td>
+              <td>
+                SOURCE
+              </td>
+              <td>
+                FileStreamSource
+              </td>
+              <td>
+                <Link
+                  className="mr-1"
+                  key="test-topic"
+                  to="/ui/clusters/local/topics/test-topic"
+                >
+                  <LinkAnchor
+                    className="mr-1"
+                    href="/ui/clusters/local/topics/test-topic"
+                    navigate={[Function]}
+                  >
+                    <a
+                      className="mr-1"
+                      href="/ui/clusters/local/topics/test-topic"
+                      onClick={[Function]}
+                    >
+                      test-topic
+                    </a>
+                  </LinkAnchor>
+                </Link>
+              </td>
+              <td>
+                <StatusTag
+                  status="RUNNING"
+                >
+                  <span
+                    className="tag is-success"
+                  >
+                    RUNNING
+                  </span>
+                </StatusTag>
+              </td>
+              <td>
+                <span
+                  className="has-text-success"
+                >
+                  2
+                   of 
+                  2
+                </span>
+              </td>
+              <td
+                className="has-text-right"
+              >
+                <Dropdown
+                  label={
+                    <span
+                      className="icon"
+                    >
+                      <i
+                        className="fas fa-cog"
+                      />
+                    </span>
+                  }
+                  right={true}
+                >
+                  <div
+                    className="dropdown is-right"
+                  >
+                    <div
+                      className="dropdown-trigger"
+                    >
+                      <button
+                        aria-controls="dropdown-menu"
+                        aria-haspopup="true"
+                        className="button is-small"
+                        onClick={[Function]}
+                        type="button"
+                      >
+                        <span
+                          className="icon"
+                        >
+                          <i
+                            className="fas fa-cog"
+                          />
+                        </span>
+                      </button>
+                    </div>
+                    <div
+                      className="dropdown-menu"
+                      id="dropdown-menu"
+                      role="menu"
+                    >
+                      <div
+                        className="dropdown-content has-text-left"
+                      >
+                        <DropdownDivider>
+                          <hr
+                            className="dropdown-divider"
+                          />
+                        </DropdownDivider>
+                        <DropdownItem
+                          onClick={[Function]}
+                        >
+                          <a
+                            className="dropdown-item is-link"
+                            href="#end"
+                            onClick={[Function]}
+                            role="menuitem"
+                            type="button"
+                          >
+                            <span
+                              className="has-text-danger"
+                            >
+                              Remove Connector
+                            </span>
+                          </a>
+                        </DropdownItem>
+                      </div>
+                    </div>
+                  </div>
+                </Dropdown>
+              </td>
+            </tr>
+          </ListItem>
+        </tbody>
+      </table>
+    </Router>
+  </BrowserRouter>
+</Provider>
+`;

+ 20 - 0
kafka-ui-react-app/src/components/Connect/StatusTag.tsx

@@ -0,0 +1,20 @@
+import cx from 'classnames';
+import { ConnectorTaskStatus } from 'generated-sources';
+import React from 'react';
+
+export interface StatusTagProps {
+  status: ConnectorTaskStatus;
+}
+
+const StatusTag: React.FC<StatusTagProps> = ({ status }) => {
+  const classNames = cx('tag', {
+    'is-success': status === ConnectorTaskStatus.RUNNING,
+    'is-success is-light': status === ConnectorTaskStatus.PAUSED,
+    'is-light': status === ConnectorTaskStatus.UNASSIGNED,
+    'is-danger': status === ConnectorTaskStatus.FAILED,
+  });
+
+  return <span className={classNames}>{status}</span>;
+};
+
+export default StatusTag;

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

@@ -12,7 +12,7 @@ const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({
   }
 
   return (
-    <tr className="cursor-pointer" onClick={goToConsumerGroupDetails}>
+    <tr className="is-clickable" onClick={goToConsumerGroupDetails}>
       <td>{consumerGroup.consumerGroupId}</td>
       <td>{consumerGroup.numConsumers}</td>
       <td>{consumerGroup.numTopics}</td>

+ 1 - 1
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx

@@ -25,7 +25,7 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
       <div className="title is-6 has-text-overflow-ellipsis">
         <div
           className={`tag has-margin-right ${
-            status === ServerStatus.ONLINE ? 'is-primary' : 'is-danger'
+            status === ServerStatus.ONLINE ? 'is-success' : 'is-danger'
           }`}
         >
           {status}

+ 1 - 1
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx

@@ -46,7 +46,7 @@ const ClustersWidget: React.FC<Props> = ({
 
       <MetricsWrapper>
         <Indicator label="Online Clusters">
-          <span className="tag is-primary">{onlineClusters.length}</span>
+          <span className="tag is-success">{onlineClusters.length}</span>
         </Indicator>
         <Indicator label="Offline Clusters">
           <span className="tag is-danger">{offlineClusters.length}</span>

+ 1 - 1
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClusterWidget.spec.tsx

@@ -11,7 +11,7 @@ describe('ClusterWidget', () => {
       const tag = shallow(<ClusterWidget cluster={onlineCluster} />).find(
         '.tag'
       );
-      expect(tag.hasClass('is-primary')).toBeTruthy();
+      expect(tag.hasClass('is-success')).toBeTruthy();
       expect(tag.text()).toEqual(ServerStatus.ONLINE);
     });
 

+ 1 - 1
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/__snapshots__/ClusterWidget.spec.tsx.snap

@@ -90,7 +90,7 @@ exports[`ClusterWidget when cluster is online matches snapshot 1`] = `
       className="title is-6 has-text-overflow-ellipsis"
     >
       <div
-        className="tag has-margin-right is-primary"
+        className="tag has-margin-right is-success"
       >
         online
       </div>

+ 1 - 1
kafka-ui-react-app/src/components/Nav/ClusterStatusIcon.tsx

@@ -17,7 +17,7 @@ const ClusterStatusIcon: React.FC<Props> = ({ status }) => {
   return (
     <span
       className={`tag ${
-        status === ServerStatus.ONLINE ? 'is-primary' : 'is-danger'
+        status === ServerStatus.ONLINE ? 'is-success' : 'is-danger'
       }`}
       title={status}
       style={style}

+ 2 - 2
kafka-ui-react-app/src/components/Nav/__tests__/ClusterStatusIcon.spec.tsx

@@ -11,12 +11,12 @@ describe('ClusterStatusIcon', () => {
 
   it('renders online icon', () => {
     const wrapper = mount(<ClusterStatusIcon status={ServerStatus.ONLINE} />);
-    expect(wrapper.exists('.is-primary')).toBeTruthy();
+    expect(wrapper.exists('.is-success')).toBeTruthy();
     expect(wrapper.exists('.is-danger')).toBeFalsy();
   });
   it('renders offline icon', () => {
     const wrapper = mount(<ClusterStatusIcon status={ServerStatus.OFFLINE} />);
     expect(wrapper.exists('.is-danger')).toBeTruthy();
-    expect(wrapper.exists('.is-primary')).toBeFalsy();
+    expect(wrapper.exists('.is-success')).toBeFalsy();
   });
 });

+ 1 - 1
kafka-ui-react-app/src/components/Nav/__tests__/__snapshots__/ClusterStatusIcon.spec.tsx.snap

@@ -5,7 +5,7 @@ exports[`ClusterStatusIcon matches snapshot 1`] = `
   status="online"
 >
   <span
-    className="tag is-primary"
+    className="tag is-success"
     style={
       Object {
         "borderRadius": "5px",

+ 1 - 1
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -86,7 +86,7 @@ const Details: React.FC<DetailsProps> = ({
                     title="in development"
                     onClick={onDelete}
                   >
-                    Delete
+                    Remove
                   </button>
                 </div>
               )}

+ 23 - 23
kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx

@@ -8,18 +8,23 @@ import DetailsContainer from '../DetailsContainer';
 import Details, { DetailsProps } from '../Details';
 import { schema, versions } from './fixtures';
 
+const clusterName = 'testCluster';
+const fetchSchemaVersionsMock = jest.fn();
+
 describe('Details', () => {
   describe('Container', () => {
     const store = configureStore();
 
     it('renders view', () => {
-      const component = shallow(
+      const wrapper = mount(
         <Provider store={store}>
-          <DetailsContainer />
+          <StaticRouter>
+            <DetailsContainer />
+          </StaticRouter>
         </Provider>
       );
 
-      expect(component.exists()).toBeTruthy();
+      expect(wrapper.exists(Details)).toBeTruthy();
     });
   });
 
@@ -28,8 +33,8 @@ describe('Details', () => {
       <Details
         subject={schema.subject}
         schema={schema}
-        clusterName="Test cluster"
-        fetchSchemaVersions={jest.fn()}
+        clusterName={clusterName}
+        fetchSchemaVersions={fetchSchemaVersionsMock}
         deleteSchema={jest.fn()}
         isFetched
         versions={[]}
@@ -37,29 +42,24 @@ describe('Details', () => {
       />
     );
     describe('Initial state', () => {
-      let useEffect: jest.SpyInstance<
-        void,
-        [effect: React.EffectCallback, deps?: React.DependencyList | undefined]
-      >;
-      const mockedFn = jest.fn();
-
-      const mockedUseEffect = () => {
-        useEffect.mockImplementationOnce(mockedFn);
-      };
-
-      beforeEach(() => {
-        useEffect = jest.spyOn(React, 'useEffect');
-        mockedUseEffect();
-        shallow(setupWrapper({ fetchSchemaVersions: mockedFn }));
-      });
-
       it('should call fetchSchemaVersions every render', () => {
-        expect(mockedFn).toHaveBeenCalled();
+        mount(
+          <StaticRouter>
+            {setupWrapper({ fetchSchemaVersions: fetchSchemaVersionsMock })}
+          </StaticRouter>
+        );
+
+        expect(fetchSchemaVersionsMock).toHaveBeenCalledWith(
+          clusterName,
+          schema.subject
+        );
       });
 
       it('matches snapshot', () => {
         expect(
-          shallow(setupWrapper({ fetchSchemaVersions: mockedFn }))
+          shallow(
+            setupWrapper({ fetchSchemaVersions: fetchSchemaVersionsMock })
+          )
         ).toMatchSnapshot();
       });
     });

+ 7 - 7
kafka-ui-react-app/src/components/Schemas/Details/__test__/__snapshots__/Details.spec.tsx.snap

@@ -11,7 +11,7 @@ exports[`Details View Initial state matches snapshot 1`] = `
       links={
         Array [
           Object {
-            "href": "/ui/clusters/Test cluster/schemas",
+            "href": "/ui/clusters/testCluster/schemas",
             "label": "Schema Registry",
           },
         ]
@@ -65,7 +65,7 @@ exports[`Details View Initial state matches snapshot 1`] = `
           title="in development"
           type="button"
         >
-          Delete
+          Remove
         </button>
       </div>
     </div>
@@ -122,7 +122,7 @@ exports[`Details View when page with schema versions is loading matches snapshot
       links={
         Array [
           Object {
-            "href": "/ui/clusters/Test cluster/schemas",
+            "href": "/ui/clusters/testCluster/schemas",
             "label": "Schema Registry",
           },
         ]
@@ -146,7 +146,7 @@ exports[`Details View when page with schema versions loaded when schema has vers
       links={
         Array [
           Object {
-            "href": "/ui/clusters/Test cluster/schemas",
+            "href": "/ui/clusters/testCluster/schemas",
             "label": "Schema Registry",
           },
         ]
@@ -200,7 +200,7 @@ exports[`Details View when page with schema versions loaded when schema has vers
           title="in development"
           type="button"
         >
-          Delete
+          Remove
         </button>
       </div>
     </div>
@@ -284,7 +284,7 @@ exports[`Details View when page with schema versions loaded when versions are em
       links={
         Array [
           Object {
-            "href": "/ui/clusters/Test cluster/schemas",
+            "href": "/ui/clusters/testCluster/schemas",
             "label": "Schema Registry",
           },
         ]
@@ -338,7 +338,7 @@ exports[`Details View when page with schema versions loaded when versions are em
           title="in development"
           type="button"
         >
-          Delete
+          Remove
         </button>
       </div>
     </div>

+ 3 - 5
kafka-ui-react-app/src/components/Topics/List/ListItem.tsx

@@ -9,7 +9,7 @@ import {
 import DropdownItem from 'components/common/Dropdown/DropdownItem';
 import Dropdown from 'components/common/Dropdown/Dropdown';
 
-interface ListItemProps {
+export interface ListItemProps {
   topic: TopicWithDetailedInfo;
   deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void;
   clusterName: ClusterName;
@@ -50,9 +50,7 @@ const ListItem: React.FC<ListItemProps> = ({
       <td>{partitions?.length}</td>
       <td>{outOfSyncReplicas}</td>
       <td>
-        <div
-          className={cx('tag is-small', internal ? 'is-light' : 'is-success')}
-        >
+        <div className={cx('tag', internal ? 'is-light' : 'is-primary')}>
           {internal ? 'Internal' : 'External'}
         </div>
       </td>
@@ -66,7 +64,7 @@ const ListItem: React.FC<ListItemProps> = ({
           right
         >
           <DropdownItem onClick={deleteTopicHandler}>
-            <span className="has-text-danger">Delete Topic</span>
+            <span className="has-text-danger">Remove Topic</span>
           </DropdownItem>
         </Dropdown>
       </td>

+ 47 - 15
kafka-ui-react-app/src/components/Topics/List/__tests__/ListItem.spec.tsx

@@ -1,21 +1,53 @@
-import { shallow } from 'enzyme';
 import React from 'react';
-import ListItem from '../ListItem';
+import { StaticRouter } from 'react-router';
+import { shallow, mount } from 'enzyme';
+import {
+  externalTopicPayload,
+  internalTopicPayload,
+} from 'redux/reducers/topics/__test__/fixtures';
+import ListItem, { ListItemProps } from '../ListItem';
+
+const mockDelete = jest.fn();
+const clusterName = 'local';
 
 describe('ListItem', () => {
-  it('triggers the deleting thunk when clicked on the delete button', () => {
-    const mockDelete = jest.fn();
-    const topic = { name: 'topic', id: 'id' };
-    const clustterName = 'cluster';
-    const component = shallow(
-      <ListItem
-        topic={topic}
-        deleteTopic={mockDelete}
-        clusterName={clustterName}
-      />
-    );
-    component.find('DropdownItem').simulate('click');
+  const setupComponent = (props: Partial<ListItemProps> = {}) => (
+    <ListItem
+      topic={internalTopicPayload}
+      deleteTopic={mockDelete}
+      clusterName={clusterName}
+      {...props}
+    />
+  );
+
+  it('triggers the deleteTopic when clicked on the delete button', () => {
+    const wrapper = shallow(setupComponent());
+    wrapper.find('DropdownItem').simulate('click');
     expect(mockDelete).toBeCalledTimes(1);
-    expect(mockDelete).toBeCalledWith(clustterName, topic.name);
+    expect(mockDelete).toBeCalledWith(clusterName, internalTopicPayload.name);
+  });
+
+  it('renders correct tags for internal topic', () => {
+    const wrapper = mount(
+      <StaticRouter>
+        <table>
+          <tbody>{setupComponent()}</tbody>
+        </table>
+      </StaticRouter>
+    );
+
+    expect(wrapper.find('.tag.is-light').text()).toEqual('Internal');
+  });
+
+  it('renders correct tags for external topic', () => {
+    const wrapper = mount(
+      <StaticRouter>
+        <table>
+          <tbody>{setupComponent({ topic: externalTopicPayload })}</tbody>
+        </table>
+      </StaticRouter>
+    );
+
+    expect(wrapper.find('.tag.is-primary').text()).toEqual('External');
   });
 });

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -33,7 +33,7 @@ const Overview: React.FC<Props> = ({
         </span>
       </Indicator>
       <Indicator label="Type">
-        <span className="tag is-primary">
+        <span className={`tag ${internal ? 'is-light' : 'is-primary'}`}>
           {internal ? 'Internal' : 'External'}
         </span>
       </Indicator>

+ 1 - 2
kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx

@@ -5,7 +5,6 @@ import {
   TopicName,
   TopicConfigByName,
   TopicWithDetailedInfo,
-  CleanupPolicy,
 } from 'redux/interfaces';
 import { TopicConfig } from 'generated-sources';
 import { useForm, FormProvider } from 'react-hook-form';
@@ -32,7 +31,7 @@ const DEFAULTS = {
   partitions: 1,
   replicationFactor: 1,
   minInSyncReplicas: 1,
-  cleanupPolicy: CleanupPolicy.Delete,
+  cleanupPolicy: 'delete',
   retentionBytes: -1,
   maxMessageBytes: 1000012,
 };

+ 4 - 4
kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { useFormContext } from 'react-hook-form';
 import { TOPIC_NAME_VALIDATION_PATTERN, BYTES_IN_GB } from 'lib/constants';
-import { CleanupPolicy, TopicName, TopicConfigByName } from 'redux/interfaces';
+import { TopicName, TopicConfigByName } from 'redux/interfaces';
 import { ErrorMessage } from '@hookform/error-message';
 import CustomParamsContainer from './CustomParams/CustomParamsContainer';
 import TimeToRetain from './TimeToRetain';
@@ -104,13 +104,13 @@ const TopicForm: React.FC<Props> = ({
           <label className="label">Cleanup policy</label>
           <div className="select is-block">
             <select
-              defaultValue={CleanupPolicy.Delete}
+              defaultValue="delete"
               name="cleanupPolicy"
               ref={register}
               disabled={isSubmitting}
             >
-              <option value={CleanupPolicy.Delete}>Delete</option>
-              <option value={CleanupPolicy.Compact}>Compact</option>
+              <option value="delete">Delete</option>
+              <option value="compact">Compact</option>
             </select>
           </div>
         </div>

+ 1 - 1
kafka-ui-react-app/src/components/__tests__/__snapshots__/App.spec.tsx.snap

@@ -295,7 +295,7 @@ exports[`App view matches snapshot 1`] = `
                                         className="title has-text-centered"
                                       >
                                         <span
-                                          className="tag is-primary"
+                                          className="tag is-success"
                                         >
                                           0
                                         </span>

+ 109 - 4
kafka-ui-react-app/src/redux/actions/__test__/thunks/connectors.spec.ts

@@ -1,6 +1,11 @@
 import fetchMock from 'fetch-mock-jest';
 import * as actions from 'redux/actions/actions';
 import * as thunks from 'redux/actions/thunks';
+import {
+  connectorsPayload,
+  connectorsServerPayload,
+  connectsPayload,
+} from 'redux/reducers/connect/__test__/fixtures';
 import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
 
 const store = mockStoreCreator;
@@ -14,15 +19,16 @@ describe('Thunks', () => {
 
   describe('fetchConnects', () => {
     it('creates GET_CONNECTS__SUCCESS when fetching connects', async () => {
-      fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, [
-        { name: 'first', address: 'localhost' },
-      ]);
+      fetchMock.getOnce(
+        `/api/clusters/${clusterName}/connects`,
+        connectsPayload
+      );
       await store.dispatch(thunks.fetchConnects(clusterName));
       expect(store.getActions()).toEqual([
         actions.fetchConnectsAction.request(),
         actions.fetchConnectsAction.success({
           ...store.getState().connect,
-          connects: [{ name: 'first', address: 'localhost' }],
+          connects: connectsPayload,
         }),
       ]);
     });
@@ -46,4 +52,103 @@ describe('Thunks', () => {
       ]);
     });
   });
+
+  describe('fetchConnectors', () => {
+    it('creates GET_CONNECTORS__SUCCESS when fetching connects', async () => {
+      fetchMock.getOnce(
+        `/api/clusters/${clusterName}/connectors`,
+        connectorsServerPayload
+      );
+      await store.dispatch(thunks.fetchConnectors(clusterName));
+      expect(store.getActions()).toEqual([
+        actions.fetchConnectorsAction.request(),
+        actions.fetchConnectorsAction.success({
+          ...store.getState().connect,
+          connectors: connectorsPayload,
+        }),
+      ]);
+    });
+
+    it('creates GET_CONNECTORS__SUCCESS when fetching connects in silent mode', async () => {
+      fetchMock.getOnce(
+        `/api/clusters/${clusterName}/connectors`,
+        connectorsServerPayload
+      );
+      await store.dispatch(thunks.fetchConnectors(clusterName, true));
+      expect(store.getActions()).toEqual([
+        actions.fetchConnectorsAction.success({
+          ...store.getState().connect,
+          connectors: connectorsPayload,
+        }),
+      ]);
+    });
+
+    it('creates GET_CONNECTORS__FAILURE', async () => {
+      fetchMock.getOnce(`/api/clusters/${clusterName}/connectors`, 404);
+      await store.dispatch(thunks.fetchConnectors(clusterName));
+      expect(store.getActions()).toEqual([
+        actions.fetchConnectorsAction.request(),
+        actions.fetchConnectorsAction.failure({
+          alert: {
+            subject: 'local-connectors',
+            title: 'Kafka Connect Connectors',
+            response: {
+              status: 404,
+              statusText: 'Not Found',
+              body: undefined,
+            },
+          },
+        }),
+      ]);
+    });
+  });
+
+  describe('deleteConnector', () => {
+    const connectName = 'first';
+    const connectorName = 'hdfs-source-connector';
+
+    it('creates DELETE_CONNECTOR__SUCCESS', async () => {
+      fetchMock.deleteOnce(
+        `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
+        {}
+      );
+      fetchMock.getOnce(
+        `/api/clusters/${clusterName}/connectors`,
+        connectorsServerPayload
+      );
+      await store.dispatch(
+        thunks.deleteConnector(clusterName, connectName, connectorName)
+      );
+      expect(store.getActions()).toEqual([
+        actions.deleteConnectorAction.request(),
+        actions.deleteConnectorAction.success({
+          ...store.getState().connect,
+        }),
+      ]);
+    });
+
+    it('creates DELETE_CONNECTOR__FAILURE', async () => {
+      fetchMock.deleteOnce(
+        `/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
+        404
+      );
+      await store.dispatch(
+        thunks.deleteConnector(clusterName, connectName, connectorName)
+      );
+      expect(store.getActions()).toEqual([
+        actions.deleteConnectorAction.request(),
+        actions.deleteConnectorAction.failure({
+          alert: {
+            subject: 'local-first-hdfs-source-connector',
+            title: 'Kafka Connect Connector Delete',
+            response: {
+              status: 404,
+              statusText: 'Not Found',
+              body: undefined,
+            },
+          },
+        }),
+      ]);
+    });
+  });
 });

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

@@ -149,3 +149,9 @@ export const fetchConnectorAction = createAsyncAction(
   'GET_CONNECTOR__SUCCESS',
   'GET_CONNECTOR__FAILURE'
 )<undefined, ConnectState, { alert?: FailurePayload }>();
+
+export const deleteConnectorAction = createAsyncAction(
+  'DELETE_CONNECTOR__REQUEST',
+  'DELETE_CONNECTOR__SUCCESS',
+  'DELETE_CONNECTOR__FAILURE'
+)<undefined, ConnectState, { alert?: FailurePayload }>();

+ 20 - 28
kafka-ui-react-app/src/redux/actions/thunks/connectors.ts

@@ -1,6 +1,5 @@
 import { KafkaConnectApi, Configuration } from 'generated-sources';
 import { BASE_PARAMS } from 'lib/constants';
-
 import {
   ClusterName,
   FailurePayload,
@@ -33,62 +32,55 @@ export const fetchConnects = (
 
 export const fetchConnectors = (
   clusterName: ClusterName,
-  connectName: string
+  silent = false
 ): PromiseThunkResult<void> => async (dispatch, getState) => {
-  dispatch(actions.fetchConnectorsAction.request());
+  if (!silent) dispatch(actions.fetchConnectorsAction.request());
   try {
-    const connectorNames = await kafkaConnectApiClient.getConnectors({
+    const connectors = await kafkaConnectApiClient.getAllConnectors({
       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`,
+      subject: [clusterName, 'connectors'].join('-'),
+      title: `Kafka Connect Connectors`,
       response,
     };
     dispatch(actions.fetchConnectorsAction.failure({ alert }));
   }
 };
 
-export const fetchConnector = (
+export const deleteConnector = (
   clusterName: ClusterName,
   connectName: string,
   connectorName: string
 ): PromiseThunkResult<void> => async (dispatch, getState) => {
-  dispatch(actions.fetchConnectorAction.request());
+  dispatch(actions.deleteConnectorAction.request());
   try {
-    const connector = await kafkaConnectApiClient.getConnector({
+    await kafkaConnectApiClient.deleteConnector({
       clusterName,
       connectName,
       connectorName,
     });
     const state = getState().connect;
-    const newState = {
-      ...state,
-      connectors: [...state.connectors, connector],
-    };
-
-    dispatch(actions.fetchConnectorAction.success(newState));
+    dispatch(
+      actions.deleteConnectorAction.success({
+        ...state,
+        connectors: state?.connectors.filter(
+          ({ name }) => name !== connectorName
+        ),
+      })
+    );
+    dispatch(fetchConnectors(clusterName, true));
   } catch (error) {
     const response = await getResponse(error);
     const alert: FailurePayload = {
-      subject: ['connect', connectName, 'connectors', connectorName].join('-'),
-      title: `Kafka Connect ${connectName}. Connector ${connectorName}`,
+      subject: [clusterName, connectName, connectorName].join('-'),
+      title: `Kafka Connect Connector Delete`,
       response,
     };
-    dispatch(actions.fetchConnectorAction.failure({ alert }));
+    dispatch(actions.deleteConnectorAction.failure({ alert }));
   }
 };

+ 2 - 2
kafka-ui-react-app/src/redux/interfaces/connect.ts

@@ -1,6 +1,6 @@
-import { Connect, Connector } from 'generated-sources';
+import { Connect, FullConnectorInfo } from 'generated-sources';
 
 export interface ConnectState {
   connects: Connect[];
-  connectors: Connector[];
+  connectors: FullConnectorInfo[];
 }

+ 1 - 4
kafka-ui-react-app/src/redux/interfaces/topic.ts

@@ -9,10 +9,7 @@ import {
 
 export type TopicName = Topic['name'];
 
-export enum CleanupPolicy {
-  Delete = 'delete',
-  Compact = 'compact',
-}
+export type CleanupPolicy = 'delete' | 'compact';
 
 export interface TopicConfigByName {
   byName: TopicConfigParams;

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

@@ -21,9 +21,9 @@ describe('Clusters reducer', () => {
         })
       )
     ).toEqual({
-      'alert-topic-2': {
+      'POST_TOPIC__FAILURE-topic-2': {
         createdAt: 1234567890,
-        id: 'alert-topic-2',
+        id: 'POST_TOPIC__FAILURE-topic-2',
         message: 'message',
         response: undefined,
         title: 'title',
@@ -39,7 +39,9 @@ describe('Clusters reducer', () => {
         alert: failurePayload1,
       })
     );
-    expect(reducer(state, dismissAlert('alert-topic-1'))).toEqual({});
+    expect(reducer(state, dismissAlert('POST_TOPIC__FAILURE-topic-1'))).toEqual(
+      {}
+    );
   });
 
   it('does not remove alert if id is wrong', () => {
@@ -50,9 +52,9 @@ describe('Clusters reducer', () => {
       })
     );
     expect(reducer(state, dismissAlert('wrong-id'))).toEqual({
-      'alert-topic-1': {
+      'POST_TOPIC__FAILURE-topic-1': {
         createdAt: 1234567890,
-        id: 'alert-topic-1',
+        id: 'POST_TOPIC__FAILURE-topic-1',
         message: 'message',
         response: undefined,
         title: 'title',

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

@@ -10,7 +10,7 @@ export const addError = (state: AlertsState, action: Action) => {
   ) {
     const { subject, title, message, response } = action.payload.alert;
 
-    const id = `alert-${subject}`;
+    const id = `${action.type}-${subject}`;
 
     return {
       ...state,

+ 56 - 0
kafka-ui-react-app/src/redux/reducers/connect/__test__/fixtures.ts

@@ -0,0 +1,56 @@
+import {
+  ConnectorTaskStatus,
+  ConnectorType,
+  FullConnectorInfo,
+} from 'generated-sources';
+
+export const connectsPayload = [
+  { name: 'first', address: 'localhost:8083' },
+  { name: 'second', address: 'localhost:8084' },
+];
+
+export const connectorsServerPayload = [
+  {
+    connect: 'first',
+    name: 'hdfs-source-connector',
+    connector_class: 'FileStreamSource',
+    type: ConnectorType.SOURCE,
+    topics: ['test-topic'],
+    status: ConnectorTaskStatus.RUNNING,
+    tasks_count: 2,
+    failed_tasks_count: 0,
+  },
+  {
+    connect: 'second',
+    name: 'hdfs2-source-connector',
+    connector_class: 'FileStreamSource',
+    type: ConnectorType.SINK,
+    topics: ['test-topic'],
+    status: ConnectorTaskStatus.FAILED,
+    tasks_count: 3,
+    failed_tasks_count: 1,
+  },
+];
+
+export const connectorsPayload: FullConnectorInfo[] = [
+  {
+    connect: 'first',
+    name: 'hdfs-source-connector',
+    connectorClass: 'FileStreamSource',
+    type: ConnectorType.SOURCE,
+    topics: ['test-topic'],
+    status: ConnectorTaskStatus.RUNNING,
+    tasksCount: 2,
+    failedTasksCount: 0,
+  },
+  {
+    connect: 'second',
+    name: 'hdfs2-source-connector',
+    connectorClass: 'FileStreamSource',
+    type: ConnectorType.SINK,
+    topics: ['test-topic'],
+    status: ConnectorTaskStatus.FAILED,
+    tasksCount: 3,
+    failedTasksCount: 1,
+  },
+];

+ 0 - 0
kafka-ui-react-app/src/redux/reducers/connect/__tests__/reducer.spec.ts → kafka-ui-react-app/src/redux/reducers/connect/__test__/reducer.spec.ts


+ 1 - 0
kafka-ui-react-app/src/redux/reducers/connect/reducer.ts

@@ -13,6 +13,7 @@ const reducer = (state = initialState, action: Action): ConnectState => {
     case getType(actions.fetchConnectsAction.success):
     case getType(actions.fetchConnectorAction.success):
     case getType(actions.fetchConnectorsAction.success):
+    case getType(actions.deleteConnectorAction.success):
       return action.payload;
     default:
       return state;

+ 43 - 0
kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts

@@ -0,0 +1,43 @@
+export const internalTopicPayload = {
+  name: '__internal.topic',
+  internal: true,
+  partitionCount: 1,
+  replicationFactor: 1,
+  replicas: 1,
+  inSyncReplicas: 1,
+  segmentSize: 0,
+  segmentCount: 1,
+  underReplicatedPartitions: 0,
+  partitions: [
+    {
+      partition: 0,
+      leader: 1,
+      replicas: [{ broker: 1, leader: false, inSync: true }],
+      offsetMax: 0,
+      offsetMin: 0,
+    },
+  ],
+};
+
+export const externalTopicPayload = {
+  name: 'external.topic',
+  internal: false,
+  partitionCount: 1,
+  replicationFactor: 1,
+  replicas: 1,
+  inSyncReplicas: 1,
+  segmentSize: 1263,
+  segmentCount: 1,
+  underReplicatedPartitions: 0,
+  partitions: [
+    {
+      partition: 0,
+      leader: 1,
+      replicas: [{ broker: 1, leader: false, inSync: true }],
+      offsetMax: 0,
+      offsetMin: 0,
+    },
+  ],
+};
+
+export const topicsPayload = [internalTopicPayload, externalTopicPayload];

+ 0 - 0
kafka-ui-react-app/src/redux/reducers/topics/__tests__/reducer.spec.ts → kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts


+ 5 - 0
kafka-ui-react-app/src/setupTests.ts

@@ -3,3 +3,8 @@ import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
 import '@testing-library/jest-dom/extend-expect';
 
 configure({ adapter: new Adapter() });
+
+jest.mock('react-redux', () => ({
+  ...jest.requireActual('react-redux'),
+  useDispatch: () => jest.fn(),
+}));

+ 16 - 19
kafka-ui-react-app/src/theme/index.scss

@@ -3,7 +3,9 @@
 @import '~bulma-switch';
 @import 'src/theme/bulma_overrides';
 
-#root, body, html {
+#root,
+body,
+html {
   width: 100%;
   position: relative;
   margin: 0;
@@ -13,28 +15,23 @@
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
   background: repeating-linear-gradient(
-    145deg,
-    rgba(0,0,0,.003),
-    rgba(0,0,0,.005) 5px,
-    rgba(0,0,0,0) 5px,
-    rgba(0,0,0,0) 10px
-  ),
-  repeating-linear-gradient(
-    -145deg,
-    rgba(0,0,0,.003),
-    rgba(0,0,0,.005) 5px,
-    rgba(0,0,0,0) 5px,
-    rgba(0,0,0,0) 10px
-  );
+      145deg,
+      rgba(0, 0, 0, 0.003),
+      rgba(0, 0, 0, 0.005) 5px,
+      rgba(0, 0, 0, 0) 5px,
+      rgba(0, 0, 0, 0) 10px
+    ),
+    repeating-linear-gradient(
+      -145deg,
+      rgba(0, 0, 0, 0.003),
+      rgba(0, 0, 0, 0.005) 5px,
+      rgba(0, 0, 0, 0) 5px,
+      rgba(0, 0, 0, 0) 10px
+    );
   background-color: $light;
-
 }
 
 code {
   font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
     monospace;
 }
-
-.cursor-pointer {
-  cursor: pointer;
-}