瀏覽代碼

chore: migrate clusters from toolkit to react-query (#2214)

Oleg Shur 3 年之前
父節點
當前提交
a4046d46ef
共有 32 個文件被更改,包括 273 次插入571 次删除
  1. 15 33
      kafka-ui-react-app/src/components/App.tsx
  2. 23 36
      kafka-ui-react-app/src/components/Cluster/Cluster.tsx
  3. 66 84
      kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx
  4. 2 0
      kafka-ui-react-app/src/components/Cluster/__tests__/fixtures.ts
  5. 1 1
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  6. 1 1
      kafka-ui-react-app/src/components/ConsumerGroups/Details/ResetOffsets/ResetOffsets.tsx
  7. 55 62
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidget.tsx
  8. 0 17
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts
  9. 12 12
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidget.spec.tsx
  10. 0 12
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidgetContainer.spec.tsx
  11. 0 27
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/fixtures.ts
  12. 5 4
      kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx
  13. 4 7
      kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx
  14. 1 1
      kafka-ui-react-app/src/components/KsqlDb/Query/Query.tsx
  15. 16 15
      kafka-ui-react-app/src/components/Nav/Nav.tsx
  16. 1 1
      kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx
  17. 22 13
      kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx
  18. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.tsx
  19. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/utils.ts
  20. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx
  21. 1 1
      kafka-ui-react-app/src/components/Topics/Topic/SendMessage/validateMessage.ts
  22. 33 58
      kafka-ui-react-app/src/components/__tests__/App.spec.tsx
  23. 1 1
      kafka-ui-react-app/src/components/common/Pagination/Pagination.tsx
  24. 8 0
      kafka-ui-react-app/src/lib/hooks/api/useClusters.ts
  25. 1 1
      kafka-ui-react-app/src/lib/hooks/useDataSaver.ts
  26. 1 1
      kafka-ui-react-app/src/redux/reducers/alerts/alertsSlice.ts
  27. 0 55
      kafka-ui-react-app/src/redux/reducers/clusters/__test__/reducer.spec.ts
  28. 0 60
      kafka-ui-react-app/src/redux/reducers/clusters/__test__/selectors.spec.ts
  29. 0 61
      kafka-ui-react-app/src/redux/reducers/clusters/clustersSlice.ts
  30. 1 1
      kafka-ui-react-app/src/redux/reducers/connect/selectors.ts
  31. 0 2
      kafka-ui-react-app/src/redux/reducers/index.ts
  32. 0 1
      kafka-ui-react-app/vite.config.ts

+ 15 - 33
kafka-ui-react-app/src/components/App.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { Suspense, useCallback } from 'react';
 import { Routes, Route, useLocation } from 'react-router-dom';
 import { GIT_TAG, GIT_COMMIT } from 'lib/constants';
 import { clusterPath, getNonExactPath } from 'lib/paths';
@@ -10,12 +10,6 @@ import Version from 'components/Version/Version';
 import Alerts from 'components/Alerts/Alerts';
 import { ThemeProvider } from 'styled-components';
 import theme from 'theme/theme';
-import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
-import {
-  fetchClusters,
-  getClusterList,
-  getAreClustersFulfilled,
-} from 'redux/reducers/clusters/clustersSlice';
 
 import * as S from './App.styled';
 import Logo from './common/Logo/Logo';
@@ -23,9 +17,6 @@ import GitIcon from './common/Icons/GitIcon';
 import DiscordIcon from './common/Icons/DiscordIcon';
 
 const App: React.FC = () => {
-  const dispatch = useAppDispatch();
-  const areClustersFulfilled = useAppSelector(getAreClustersFulfilled);
-  const clusters = useAppSelector(getClusterList);
   const [isSidebarVisible, setIsSidebarVisible] = React.useState(false);
   const onBurgerClick = () => setIsSidebarVisible(!isSidebarVisible);
   const closeSidebar = useCallback(() => setIsSidebarVisible(false), []);
@@ -35,10 +26,6 @@ const App: React.FC = () => {
     closeSidebar();
   }, [location, closeSidebar]);
 
-  React.useEffect(() => {
-    dispatch(fetchClusters());
-  }, [dispatch]);
-
   return (
     <ThemeProvider theme={theme}>
       <S.Layout>
@@ -90,10 +77,9 @@ const App: React.FC = () => {
 
         <S.Container>
           <S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
-            <Nav
-              clusters={clusters}
-              areClustersFulfilled={areClustersFulfilled}
-            />
+            <Suspense fallback={<PageLoader />}>
+              <Nav />
+            </Suspense>
           </S.Sidebar>
           <S.Overlay
             $visible={isSidebarVisible}
@@ -103,23 +89,19 @@ const App: React.FC = () => {
             aria-hidden="true"
             aria-label="Overlay"
           />
-          {areClustersFulfilled ? (
-            <Routes>
-              {['/', '/ui', '/ui/clusters'].map((path) => (
-                <Route
-                  key="Home" // optional: avoid full re-renders on route changes
-                  path={path}
-                  element={<Dashboard />}
-                />
-              ))}
+          <Routes>
+            {['/', '/ui', '/ui/clusters'].map((path) => (
               <Route
-                path={getNonExactPath(clusterPath())}
-                element={<ClusterPage />}
+                key="Home" // optional: avoid full re-renders on route changes
+                path={path}
+                element={<Dashboard />}
               />
-            </Routes>
-          ) : (
-            <PageLoader />
-          )}
+            ))}
+            <Route
+              path={getNonExactPath(clusterPath())}
+              element={<ClusterPage />}
+            />
+          </Routes>
         </S.Container>
         <S.AlertsContainer role="toolbar">
           <Alerts />

+ 23 - 36
kafka-ui-react-app/src/components/Cluster/Cluster.tsx

@@ -1,12 +1,7 @@
 import React, { Suspense } from 'react';
-import { useSelector } from 'react-redux';
 import { Routes, Navigate, Route, Outlet } from 'react-router-dom';
 import useAppParams from 'lib/hooks/useAppParams';
 import { ClusterFeaturesEnum } from 'generated-sources';
-import {
-  getClustersFeatures,
-  getClustersReadonlyStatus,
-} from 'redux/reducers/clusters/clustersSlice';
 import {
   clusterBrokerRelativePath,
   clusterConnectorsRelativePath,
@@ -23,6 +18,7 @@ import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
 import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
 import { BreadcrumbProvider } from 'components/common/Breadcrumb/Breadcrumb.provider';
 import PageLoader from 'components/common/PageLoader/PageLoader';
+import useClusters from 'lib/hooks/api/useClusters';
 
 const Brokers = React.lazy(() => import('components/Brokers/Brokers'));
 const Topics = React.lazy(() => import('components/Topics/Topics'));
@@ -35,34 +31,25 @@ const ConsumerGroups = React.lazy(
 
 const Cluster: React.FC = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();
-  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 isTopicDeletionAllowed = features.includes(
-    ClusterFeaturesEnum.TOPIC_DELETION
-  );
-  const hasKsqlDbConfigured = features.includes(ClusterFeaturesEnum.KSQL_DB);
+  const { data } = useClusters();
+  const contextValue = React.useMemo(() => {
+    const cluster = data?.find(({ name }) => name === clusterName);
+    const features = cluster?.features || [];
 
-  const contextValue = React.useMemo(
-    () => ({
-      isReadOnly,
-      hasKafkaConnectConfigured,
-      hasSchemaRegistryConfigured,
-      isTopicDeletionAllowed,
-    }),
-    [
-      hasKafkaConnectConfigured,
-      hasSchemaRegistryConfigured,
-      isReadOnly,
-      isTopicDeletionAllowed,
-    ]
-  );
+    return {
+      isReadOnly: cluster?.readOnly || false,
+      hasKafkaConnectConfigured: features.includes(
+        ClusterFeaturesEnum.KAFKA_CONNECT
+      ),
+      hasSchemaRegistryConfigured: features.includes(
+        ClusterFeaturesEnum.SCHEMA_REGISTRY
+      ),
+      isTopicDeletionAllowed: features.includes(
+        ClusterFeaturesEnum.TOPIC_DELETION
+      ),
+      hasKsqlDbConfigured: features.includes(ClusterFeaturesEnum.KSQL_DB),
+    };
+  }, [clusterName, data]);
 
   return (
     <BreadcrumbProvider>
@@ -94,7 +81,7 @@ const Cluster: React.FC = () => {
                 </BreadcrumbRoute>
               }
             />
-            {hasSchemaRegistryConfigured && (
+            {contextValue.hasSchemaRegistryConfigured && (
               <Route
                 path={getNonExactPath(clusterSchemasRelativePath)}
                 element={
@@ -104,7 +91,7 @@ const Cluster: React.FC = () => {
                 }
               />
             )}
-            {hasKafkaConnectConfigured && (
+            {contextValue.hasKafkaConnectConfigured && (
               <Route
                 path={getNonExactPath(clusterConnectsRelativePath)}
                 element={
@@ -114,7 +101,7 @@ const Cluster: React.FC = () => {
                 }
               />
             )}
-            {hasKafkaConnectConfigured && (
+            {contextValue.hasKafkaConnectConfigured && (
               <Route
                 path={getNonExactPath(clusterConnectorsRelativePath)}
                 element={
@@ -124,7 +111,7 @@ const Cluster: React.FC = () => {
                 }
               />
             )}
-            {hasKsqlDbConfigured && (
+            {contextValue.hasKsqlDbConfigured && (
               <Route
                 path={getNonExactPath(clusterKsqlDbRelativePath)}
                 element={

+ 66 - 84
kafka-ui-react-app/src/components/Cluster/__tests__/Cluster.spec.tsx

@@ -1,13 +1,11 @@
 import React from 'react';
-import { ClusterFeaturesEnum } from 'generated-sources';
-import { store } from 'redux/store';
-import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
-import Cluster from 'components/Cluster/Cluster';
-import { fetchClusters } from 'redux/reducers/clusters/clustersSlice';
-import { screen } from '@testing-library/react';
+import { Cluster, ClusterFeaturesEnum } from 'generated-sources';
+import ClusterComponent from 'components/Cluster/Cluster';
+import { screen, waitFor } from '@testing-library/react';
 import { render, WithRoute } from 'lib/testHelpers';
 import {
   clusterBrokersPath,
+  clusterConnectorsPath,
   clusterConnectsPath,
   clusterConsumerGroupsPath,
   clusterKsqlDbPath,
@@ -16,6 +14,9 @@ import {
   clusterTopicsPath,
 } from 'lib/paths';
 import { act } from 'react-dom/test-utils';
+import fetchMock from 'fetch-mock';
+
+import { onlineClusterPayload } from './fixtures';
 
 const CLusterCompText = {
   Topics: 'Topics',
@@ -46,98 +47,79 @@ jest.mock('components/KsqlDb/KsqlDb', () => () => (
 ));
 
 describe('Cluster', () => {
-  const renderComponent = (pathname: string) => {
-    render(
-      <WithRoute path={`${clusterPath()}/*`}>
-        <Cluster />
-      </WithRoute>,
-      { initialEntries: [pathname], store }
-    );
+  afterEach(() => fetchMock.restore());
+
+  const renderComponent = async (pathname: string, payload: Cluster[] = []) => {
+    const mock = fetchMock.get('/api/clusters', payload);
+    await act(() => {
+      render(
+        <WithRoute path={`${clusterPath()}/*`}>
+          <ClusterComponent />
+        </WithRoute>,
+        { initialEntries: [pathname] }
+      );
+    });
+    return waitFor(() => expect(mock.called()).toBeTruthy());
   };
 
   it('renders Brokers', async () => {
-    await act(() => renderComponent(clusterBrokersPath('second')));
+    await renderComponent(clusterBrokersPath('second'));
     expect(screen.getByText(CLusterCompText.Brokers)).toBeInTheDocument();
   });
   it('renders Topics', async () => {
-    await act(() => renderComponent(clusterTopicsPath('second')));
+    await renderComponent(clusterTopicsPath('second'));
     expect(screen.getByText(CLusterCompText.Topics)).toBeInTheDocument();
   });
   it('renders ConsumerGroups', async () => {
-    await act(() => renderComponent(clusterConsumerGroupsPath('second')));
+    await renderComponent(clusterConsumerGroupsPath('second'));
     expect(
       screen.getByText(CLusterCompText.ConsumerGroups)
     ).toBeInTheDocument();
   });
 
   describe('configured features', () => {
-    it('does not render Schemas if SCHEMA_REGISTRY is not configured', async () => {
-      store.dispatch(
-        fetchClusters.fulfilled(
-          [
-            {
-              ...onlineClusterPayload,
-              features: [],
-            },
-          ],
-          '123'
-        )
-      );
-      await act(() => renderComponent(clusterSchemasPath('second')));
-      expect(
-        screen.queryByText(CLusterCompText.Schemas)
-      ).not.toBeInTheDocument();
-    });
-    it('renders Schemas if SCHEMA_REGISTRY is configured', async () => {
-      store.dispatch(
-        fetchClusters.fulfilled(
-          [
-            {
-              ...onlineClusterPayload,
-              features: [ClusterFeaturesEnum.SCHEMA_REGISTRY],
-            },
-          ],
-          '123'
-        )
-      );
-      await act(() =>
-        renderComponent(clusterSchemasPath(onlineClusterPayload.name))
-      );
-      expect(screen.getByText(CLusterCompText.Schemas)).toBeInTheDocument();
-    });
-    it('renders Connect if KAFKA_CONNECT is configured', async () => {
-      store.dispatch(
-        fetchClusters.fulfilled(
-          [
-            {
-              ...onlineClusterPayload,
-              features: [ClusterFeaturesEnum.KAFKA_CONNECT],
-            },
-          ],
-          'requestId'
-        )
-      );
-      await act(() =>
-        renderComponent(clusterConnectsPath(onlineClusterPayload.name))
-      );
-      expect(screen.getByText(CLusterCompText.Connect)).toBeInTheDocument();
-    });
-    it('renders KSQL if KSQL_DB is configured', async () => {
-      store.dispatch(
-        fetchClusters.fulfilled(
-          [
-            {
-              ...onlineClusterPayload,
-              features: [ClusterFeaturesEnum.KSQL_DB],
-            },
-          ],
-          'requestId'
-        )
-      );
-      await act(() =>
-        renderComponent(clusterKsqlDbPath(onlineClusterPayload.name))
-      );
-      expect(screen.getByText(CLusterCompText.KsqlDb)).toBeInTheDocument();
-    });
+    const itCorrectlyHandlesConfiguredSchema = (
+      feature: ClusterFeaturesEnum,
+      text: string,
+      path: string
+    ) => {
+      it(`renders Schemas if ${feature} is configured`, async () => {
+        await renderComponent(path, [
+          {
+            ...onlineClusterPayload,
+            features: [feature],
+          },
+        ]);
+        expect(screen.getByText(text)).toBeInTheDocument();
+      });
+
+      it(`does not render Schemas if ${feature} is not configured`, async () => {
+        await renderComponent(path, [
+          { ...onlineClusterPayload, features: [] },
+        ]);
+        expect(screen.queryByText(text)).not.toBeInTheDocument();
+      });
+    };
+
+    itCorrectlyHandlesConfiguredSchema(
+      ClusterFeaturesEnum.SCHEMA_REGISTRY,
+      CLusterCompText.Schemas,
+      clusterSchemasPath(onlineClusterPayload.name)
+    );
+    itCorrectlyHandlesConfiguredSchema(
+      ClusterFeaturesEnum.KAFKA_CONNECT,
+      CLusterCompText.Connect,
+      clusterConnectsPath(onlineClusterPayload.name)
+    );
+    itCorrectlyHandlesConfiguredSchema(
+      ClusterFeaturesEnum.KAFKA_CONNECT,
+      CLusterCompText.Connect,
+      clusterConnectorsPath(onlineClusterPayload.name)
+    );
+    itCorrectlyHandlesConfiguredSchema(
+      ClusterFeaturesEnum.KSQL_DB,
+      CLusterCompText.KsqlDb,
+      clusterKsqlDbPath(onlineClusterPayload.name)
+    );
   });
 });

+ 2 - 0
kafka-ui-react-app/src/redux/reducers/clusters/__test__/fixtures.ts → kafka-ui-react-app/src/components/Cluster/__tests__/fixtures.ts

@@ -9,6 +9,7 @@ export const onlineClusterPayload: Cluster = {
   topicCount: 3,
   bytesInPerSec: 1.55,
   bytesOutPerSec: 9.314,
+  readOnly: false,
   features: [],
 };
 export const offlineClusterPayload: Cluster = {
@@ -21,6 +22,7 @@ export const offlineClusterPayload: Cluster = {
   bytesInPerSec: 3.42,
   bytesOutPerSec: 4.14,
   features: [],
+  readOnly: true,
 };
 
 export const clustersPayload: Cluster[] = [

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

@@ -14,7 +14,7 @@ import * as Metrics from 'components/common/Metrics';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import Dropdown from 'components/common/Dropdown/Dropdown';
 import DropdownItem from 'components/common/Dropdown/DropdownItem';
-import { groupBy } from 'lodash';
+import groupBy from 'lodash/groupBy';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';

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

@@ -12,7 +12,7 @@ import MultiSelect from 'react-multi-select-component';
 import { Option } from 'react-multi-select-component/dist/lib/interfaces';
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
-import { groupBy } from 'lodash';
+import groupBy from 'lodash/groupBy';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import { ErrorMessage } from '@hookform/error-message';
 import Select from 'components/common/Select/Select';

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

@@ -1,7 +1,5 @@
 import React from 'react';
-import { chunk } from 'lodash';
 import * as Metrics from 'components/common/Metrics';
-import { Cluster } from 'generated-sources';
 import { Tag } from 'components/common/Tag/Tag.styled';
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
@@ -9,41 +7,38 @@ import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
 import { NavLink } from 'react-router-dom';
 import { clusterTopicsPath } from 'lib/paths';
 import Switch from 'components/common/Switch/Switch';
+import useClusters from 'lib/hooks/api/useClusters';
+import { ServerStatus } from 'generated-sources';
 
 import * as S from './ClustersWidget.styled';
 
-interface Props {
-  clusters: Cluster[];
-  onlineClusters: Cluster[];
-  offlineClusters: Cluster[];
-}
-
-const ClustersWidget: React.FC<Props> = ({
-  clusters,
-  onlineClusters,
-  offlineClusters,
-}) => {
+const ClustersWidget: React.FC = () => {
+  const { data } = useClusters();
   const [showOfflineOnly, setShowOfflineOnly] = React.useState<boolean>(false);
 
-  const clusterList = React.useMemo(() => {
-    if (showOfflineOnly) {
-      return chunk(offlineClusters, 2);
-    }
-    return chunk(clusters, 2);
-  }, [clusters, offlineClusters, showOfflineOnly]);
+  const config = React.useMemo(() => {
+    const clusters = data || [];
+    const offlineClusters = clusters.filter(
+      ({ status }) => status === ServerStatus.OFFLINE
+    );
+    return {
+      list: showOfflineOnly ? offlineClusters : clusters,
+      online: clusters.length - offlineClusters.length,
+      offline: offlineClusters.length,
+    };
+  }, [data, showOfflineOnly]);
 
   const handleSwitch = () => setShowOfflineOnly(!showOfflineOnly);
-
   return (
     <>
       <Metrics.Wrapper>
         <Metrics.Section>
           <Metrics.Indicator label={<Tag color="green">Online</Tag>}>
-            <span>{onlineClusters.length}</span>{' '}
+            <span>{config.online}</span>{' '}
             <Metrics.LightText>clusters</Metrics.LightText>
           </Metrics.Indicator>
           <Metrics.Indicator label={<Tag color="gray">Offline</Tag>}>
-            <span>{offlineClusters.length}</span>{' '}
+            <span>{config.offline}</span>{' '}
             <Metrics.LightText>clusters</Metrics.LightText>
           </Metrics.Indicator>
         </Metrics.Section>
@@ -56,47 +51,45 @@ const ClustersWidget: React.FC<Props> = ({
         />
         <label>Only offline clusters</label>
       </S.SwitchWrapper>
-      {clusterList.map((chunkItem) => (
-        <Table key={chunkItem.map(({ name }) => name).join('-')} isFullwidth>
-          <thead>
-            <tr>
-              <TableHeaderCell title="Cluster name" />
-              <TableHeaderCell title="Version" />
-              <TableHeaderCell title="Brokers count" />
-              <TableHeaderCell title="Partitions" />
-              <TableHeaderCell title="Topics" />
-              <TableHeaderCell title="Production" />
-              <TableHeaderCell title="Consumption" />
+      <Table isFullwidth>
+        <thead>
+          <tr>
+            <TableHeaderCell title="Cluster name" />
+            <TableHeaderCell title="Version" />
+            <TableHeaderCell title="Brokers count" />
+            <TableHeaderCell title="Partitions" />
+            <TableHeaderCell title="Topics" />
+            <TableHeaderCell title="Production" />
+            <TableHeaderCell title="Consumption" />
+          </tr>
+        </thead>
+        <tbody>
+          {config.list.map((cluster) => (
+            <tr key={cluster.name}>
+              <S.TableCell maxWidth="99px" width="350">
+                {cluster.readOnly && <Tag color="blue">readonly</Tag>}{' '}
+                {cluster.name}
+              </S.TableCell>
+              <S.TableCell maxWidth="99px">{cluster.version}</S.TableCell>
+              <S.TableCell maxWidth="99px">{cluster.brokerCount}</S.TableCell>
+              <S.TableCell maxWidth="78px">
+                {cluster.onlinePartitionCount}
+              </S.TableCell>
+              <S.TableCell maxWidth="60px">
+                <NavLink to={clusterTopicsPath(cluster.name)}>
+                  {cluster.topicCount}
+                </NavLink>
+              </S.TableCell>
+              <S.TableCell maxWidth="85px">
+                <BytesFormatted value={cluster.bytesInPerSec} />
+              </S.TableCell>
+              <S.TableCell maxWidth="85px">
+                <BytesFormatted value={cluster.bytesOutPerSec} />
+              </S.TableCell>
             </tr>
-          </thead>
-          <tbody>
-            {chunkItem.map((cluster) => (
-              <tr key={cluster.name}>
-                <S.TableCell maxWidth="99px" width="350">
-                  {cluster.readOnly && <Tag color="blue">readonly</Tag>}{' '}
-                  {cluster.name}
-                </S.TableCell>
-                <S.TableCell maxWidth="99px">{cluster.version}</S.TableCell>
-                <S.TableCell maxWidth="99px">{cluster.brokerCount}</S.TableCell>
-                <S.TableCell maxWidth="78px">
-                  {cluster.onlinePartitionCount}
-                </S.TableCell>
-                <S.TableCell maxWidth="60px">
-                  <NavLink to={clusterTopicsPath(cluster.name)}>
-                    {cluster.topicCount}
-                  </NavLink>
-                </S.TableCell>
-                <S.TableCell maxWidth="85px">
-                  <BytesFormatted value={cluster.bytesInPerSec} />
-                </S.TableCell>
-                <S.TableCell maxWidth="85px">
-                  <BytesFormatted value={cluster.bytesOutPerSec} />
-                </S.TableCell>
-              </tr>
-            ))}
-          </tbody>
-        </Table>
-      ))}
+          ))}
+        </tbody>
+      </Table>
     </>
   );
 };

+ 0 - 17
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts

@@ -1,17 +0,0 @@
-import { connect } from 'react-redux';
-import {
-  getClusterList,
-  getOnlineClusters,
-  getOfflineClusters,
-} from 'redux/reducers/clusters/clustersSlice';
-import { RootState } from 'redux/interfaces';
-
-import ClustersWidget from './ClustersWidget';
-
-const mapStateToProps = (state: RootState) => ({
-  clusters: getClusterList(state),
-  onlineClusters: getOnlineClusters(state),
-  offlineClusters: getOfflineClusters(state),
-});
-
-export default connect(mapStateToProps)(ClustersWidget);

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

@@ -1,22 +1,22 @@
 import React from 'react';
-import { screen } from '@testing-library/react';
+import { act, screen, waitFor } from '@testing-library/react';
 import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
 import userEvent from '@testing-library/user-event';
 import { render } from 'lib/testHelpers';
+import fetchMock from 'fetch-mock';
+import { clustersPayload } from 'components/Cluster/__tests__/fixtures';
 
-import { offlineCluster, onlineCluster, clusters } from './fixtures';
+describe('ClustersWidget', () => {
+  afterEach(() => fetchMock.restore());
 
-const setupComponent = () =>
-  render(
-    <ClustersWidget
-      clusters={clusters}
-      onlineClusters={[onlineCluster]}
-      offlineClusters={[offlineCluster]}
-    />
-  );
+  beforeEach(async () => {
+    const mock = fetchMock.get('/api/clusters', clustersPayload);
 
-describe('ClustersWidget', () => {
-  beforeEach(() => setupComponent());
+    await act(() => {
+      render(<ClustersWidget />);
+    });
+    await waitFor(() => expect(mock.called()).toBeTruthy());
+  });
 
   it('renders clusterWidget list', () => {
     expect(screen.getAllByRole('row').length).toBe(3);

+ 0 - 12
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/ClustersWidgetContainer.spec.tsx

@@ -1,12 +0,0 @@
-import React from 'react';
-import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
-import { getByTextContent, render } from 'lib/testHelpers';
-
-describe('ClustersWidgetContainer', () => {
-  it('renders ClustersWidget', () => {
-    render(
-      <ClustersWidget clusters={[]} onlineClusters={[]} offlineClusters={[]} />
-    );
-    expect(getByTextContent('Online 0 clusters')).toBeInTheDocument();
-  });
-});

+ 0 - 27
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/fixtures.ts

@@ -1,27 +0,0 @@
-import { Cluster, ServerStatus } from 'generated-sources';
-
-export const onlineCluster: Cluster = {
-  name: 'secondLocal',
-  defaultCluster: false,
-  status: ServerStatus.ONLINE,
-  brokerCount: 1,
-  onlinePartitionCount: 6,
-  topicCount: 3,
-  bytesInPerSec: 0.00003061819685376471,
-  bytesOutPerSec: 5.737800890036267,
-  readOnly: false,
-};
-
-export const offlineCluster: Cluster = {
-  name: 'local',
-  defaultCluster: true,
-  status: ServerStatus.OFFLINE,
-  brokerCount: 1,
-  onlinePartitionCount: 2,
-  topicCount: 2,
-  bytesInPerSec: 8000.00000673768,
-  bytesOutPerSec: 0.8153063567297119,
-  readOnly: true,
-};
-
-export const clusters: Cluster[] = [onlineCluster, offlineCluster];

+ 5 - 4
kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx

@@ -1,12 +1,13 @@
-import React from 'react';
+import React, { Suspense } from 'react';
 import PageHeading from 'components/common/PageHeading/PageHeading';
-
-import ClustersWidgetContainer from './ClustersWidget/ClustersWidgetContainer';
+import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
 
 const Dashboard: React.FC = () => (
   <>
     <PageHeading text="Dashboard" />
-    <ClustersWidgetContainer />
+    <Suspense>
+      <ClustersWidget />
+    </Suspense>
   </>
 );
 

+ 4 - 7
kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx

@@ -3,17 +3,14 @@ import Dashboard from 'components/Dashboard/Dashboard';
 import { render } from 'lib/testHelpers';
 import { screen } from '@testing-library/dom';
 
-jest.mock(
-  'components/Dashboard/ClustersWidget/ClustersWidgetContainer.ts',
-  () => () => <div>mock-ClustersWidgetContainer</div>
-);
+jest.mock('components/Dashboard/ClustersWidget/ClustersWidget', () => () => (
+  <div>mock-ClustersWidget</div>
+));
 
 describe('Dashboard', () => {
   it('renders ClustersWidget', () => {
     render(<Dashboard />);
     expect(screen.getByText('Dashboard')).toBeInTheDocument();
-    expect(
-      screen.getByText('mock-ClustersWidgetContainer')
-    ).toBeInTheDocument();
+    expect(screen.getByText('mock-ClustersWidget')).toBeInTheDocument();
   });
 });

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

@@ -10,7 +10,7 @@ import { getKsqlExecution } from 'redux/reducers/ksqlDb/selectors';
 import { BASE_PARAMS } from 'lib/constants';
 import { KsqlResponse, KsqlTableResponse } from 'generated-sources';
 import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice';
-import { now } from 'lodash';
+import now from 'lodash/now';
 import { ClusterNameRoute } from 'lib/paths';
 
 import type { FormValues } from './QueryForm/QueryForm';

+ 16 - 15
kafka-ui-react-app/src/components/Nav/Nav.tsx

@@ -1,30 +1,31 @@
+import useClusters from 'lib/hooks/api/useClusters';
 import React from 'react';
-import { Cluster } from 'generated-sources';
 
 import ClusterMenu from './ClusterMenu';
 import ClusterMenuItem from './ClusterMenuItem';
 import * as S from './Nav.styled';
 
-interface Props {
-  areClustersFulfilled?: boolean;
-  clusters: Cluster[];
-}
+const Nav: React.FC = () => {
+  const query = useClusters();
 
-const Nav: React.FC<Props> = ({ areClustersFulfilled, clusters }) => (
-  <aside aria-label="Sidebar Menu">
-    <S.List>
-      <ClusterMenuItem to="/" title="Dashboard" isTopLevel />
-    </S.List>
+  if (!query.isSuccess) {
+    return null;
+  }
 
-    {areClustersFulfilled &&
-      clusters.map((cluster) => (
+  return (
+    <aside aria-label="Sidebar Menu">
+      <S.List>
+        <ClusterMenuItem to="/" title="Dashboard" isTopLevel />
+      </S.List>
+      {query.data.map((cluster) => (
         <ClusterMenu
           cluster={cluster}
           key={cluster.name}
-          singleMode={clusters.length === 1}
+          singleMode={query.data.length === 1}
         />
       ))}
-  </aside>
-);
+    </aside>
+  );
+};
 
 export default Nav;

+ 1 - 1
kafka-ui-react-app/src/components/Nav/__tests__/ClusterMenu.spec.tsx

@@ -1,11 +1,11 @@
 import React from 'react';
 import { screen } from '@testing-library/react';
 import { Cluster, ClusterFeaturesEnum } from 'generated-sources';
-import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
 import ClusterMenu from 'components/Nav/ClusterMenu';
 import userEvent from '@testing-library/user-event';
 import { clusterConnectorsPath } from 'lib/paths';
 import { render } from 'lib/testHelpers';
+import { onlineClusterPayload } from 'components/Cluster/__tests__/fixtures';
 
 describe('ClusterMenu', () => {
   const setupComponent = (cluster: Cluster, singleMode?: boolean) => (

+ 22 - 13
kafka-ui-react-app/src/components/Nav/__tests__/Nav.spec.tsx

@@ -1,29 +1,38 @@
 import React from 'react';
+import Nav from 'components/Nav/Nav';
+import { screen, waitFor } from '@testing-library/react';
+import { render } from 'lib/testHelpers';
 import {
   offlineClusterPayload,
   onlineClusterPayload,
-} from 'redux/reducers/clusters/__test__/fixtures';
-import Nav from 'components/Nav/Nav';
-import { screen } from '@testing-library/react';
-import { render } from 'lib/testHelpers';
+} from 'components/Cluster/__tests__/fixtures';
+import fetchMock from 'fetch-mock';
+import { act } from 'react-dom/test-utils';
+import { Cluster } from 'generated-sources';
 
 describe('Nav', () => {
+  afterEach(() => fetchMock.restore());
+
+  const renderComponent = async (payload: Cluster[] = []) => {
+    const mock = fetchMock.get('/api/clusters', payload);
+    await act(() => {
+      render(<Nav />);
+    });
+    return waitFor(() => expect(mock.called()).toBeTruthy());
+  };
+
   const getDashboard = () => screen.getByText('Dashboard');
 
   const getMenuItemsCount = () => screen.getAllByRole('menuitem').length;
-  it('renders loader', () => {
-    render(<Nav clusters={[]} />);
+  it('renders loader', async () => {
+    await renderComponent();
+
     expect(getMenuItemsCount()).toEqual(1);
     expect(getDashboard()).toBeInTheDocument();
   });
 
-  it('renders ClusterMenu', () => {
-    render(
-      <Nav
-        clusters={[onlineClusterPayload, offlineClusterPayload]}
-        areClustersFulfilled
-      />
-    );
+  it('renders ClusterMenu', async () => {
+    await renderComponent([onlineClusterPayload, offlineClusterPayload]);
     expect(screen.getAllByRole('menu').length).toEqual(3);
     expect(getMenuItemsCount()).toEqual(3);
     expect(getDashboard()).toBeInTheDocument();

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

@@ -11,7 +11,7 @@ import {
   TopicMessageEventTypeEnum,
 } from 'generated-sources';
 import React, { useContext } from 'react';
-import { omitBy } from 'lodash';
+import omitBy from 'lodash/omitBy';
 import { useNavigate, useLocation } from 'react-router-dom';
 import MultiSelect from 'components/common/MultiSelect/MultiSelect.styled';
 import { Option } from 'react-multi-select-component/dist/lib/interfaces';

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/utils.ts

@@ -1,5 +1,5 @@
 import { Partition, SeekType } from 'generated-sources';
-import { compact } from 'lodash';
+import compact from 'lodash/compact';
 import { Option } from 'react-multi-select-component/dist/lib/interfaces';
 
 export const filterOptions = (options: Option[], filter: string) => {

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

@@ -12,7 +12,7 @@ import {
 } from 'redux/reducers/topics/topicsSlice';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
 import { alertAdded } from 'redux/reducers/alerts/alertsSlice';
-import { now } from 'lodash';
+import now from 'lodash/now';
 import { Button } from 'components/common/Button/Button';
 import Editor from 'components/common/Editor/Editor';
 import PageLoader from 'components/common/PageLoader/PageLoader';

+ 1 - 1
kafka-ui-react-app/src/components/Topics/Topic/SendMessage/validateMessage.ts

@@ -1,6 +1,6 @@
 import { TopicMessageSchema } from 'generated-sources';
 import Ajv, { DefinedError } from 'ajv/dist/2020';
-import { upperFirst } from 'lodash';
+import upperFirst from 'lodash/upperFirst';
 
 const validateBySchema = (
   value: string,

+ 33 - 58
kafka-ui-react-app/src/components/__tests__/App.spec.tsx

@@ -1,74 +1,49 @@
 import React from 'react';
-import { screen, within, act } from '@testing-library/react';
+import { screen, within } from '@testing-library/react';
 import App from 'components/App';
 import { render } from 'lib/testHelpers';
-import { clustersPayload } from 'redux/reducers/clusters/__test__/fixtures';
 import userEvent from '@testing-library/user-event';
-import fetchMock from 'fetch-mock';
 
 const burgerButtonOptions = { name: 'burger' };
 const logoutButtonOptions = { name: 'Log out' };
 
+jest.mock('components/Nav/Nav', () => () => <div>Navigation</div>);
+
 describe('App', () => {
-  describe('initial state', () => {
-    beforeEach(() => {
-      render(<App />, {
-        initialEntries: ['/'],
-      });
-    });
-    it('shows PageLoader until clusters are fulfilled', () => {
-      expect(screen.getByText('Dashboard')).toBeInTheDocument();
-      expect(screen.getByRole('progressbar')).toBeInTheDocument();
-    });
-    it('correctly renders header', () => {
-      const header = screen.getByLabelText('Page Header');
-      expect(header).toBeInTheDocument();
-      expect(
-        within(header).getByText('UI for Apache Kafka')
-      ).toBeInTheDocument();
-      expect(within(header).getAllByRole('separator').length).toEqual(3);
-      expect(
-        within(header).getByRole('button', burgerButtonOptions)
-      ).toBeInTheDocument();
-      expect(
-        within(header).getByRole('button', logoutButtonOptions)
-      ).toBeInTheDocument();
-    });
-    it('handle burger click correctly', () => {
-      const header = screen.getByLabelText('Page Header');
-      const burger = within(header).getByRole('button', burgerButtonOptions);
-      const sidebar = screen.getByLabelText('Sidebar');
-      const overlay = screen.getByLabelText('Overlay');
-      expect(sidebar).toBeInTheDocument();
-      expect(overlay).toBeInTheDocument();
-      expect(overlay).toHaveStyleRule('visibility: hidden');
-      expect(burger).toHaveStyleRule('display: none');
-      userEvent.click(burger);
-      expect(overlay).toHaveStyleRule('visibility: visible');
+  beforeEach(() => {
+    render(<App />, {
+      initialEntries: ['/'],
     });
   });
 
-  describe('with clusters list fetched', () => {
-    it('shows Cluster list', async () => {
-      const mock = fetchMock.getOnce('/api/clusters', clustersPayload);
-      await act(() => {
-        render(<App />, {
-          initialEntries: ['/'],
-        });
-      });
+  it('correctly renders header', () => {
+    const header = screen.getByLabelText('Page Header');
+    expect(header).toBeInTheDocument();
+    expect(within(header).getByText('UI for Apache Kafka')).toBeInTheDocument();
+    expect(within(header).getAllByRole('separator').length).toEqual(3);
+    expect(
+      within(header).getByRole('button', burgerButtonOptions)
+    ).toBeInTheDocument();
+    expect(
+      within(header).getByRole('button', logoutButtonOptions)
+    ).toBeInTheDocument();
+  });
 
-      expect(mock.called()).toBeTruthy();
+  it('handle burger click correctly', () => {
+    const burger = within(screen.getByLabelText('Page Header')).getByRole(
+      'button',
+      burgerButtonOptions
+    );
+    const overlay = screen.getByLabelText('Overlay');
+    expect(screen.getByLabelText('Sidebar')).toBeInTheDocument();
+    expect(overlay).toBeInTheDocument();
+    expect(overlay).toHaveStyleRule('visibility: hidden');
+    expect(burger).toHaveStyleRule('display: none');
+    userEvent.click(burger);
+    expect(overlay).toHaveStyleRule('visibility: visible');
+  });
 
-      const menuContainer = screen.getByLabelText('Sidebar Menu');
-      expect(menuContainer).toBeInTheDocument();
-      expect(within(menuContainer).getByText('Dashboard')).toBeInTheDocument();
-      expect(
-        within(menuContainer).getByText(clustersPayload[0].name)
-      ).toBeInTheDocument();
-      expect(
-        within(menuContainer).getByText(clustersPayload[1].name)
-      ).toBeInTheDocument();
-      expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
-    });
+  it('Renders navigation', async () => {
+    expect(screen.getByText('Navigation')).toBeInTheDocument();
   });
 });

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

@@ -1,6 +1,6 @@
 import { PER_PAGE } from 'lib/constants';
 import usePagination from 'lib/hooks/usePagination';
-import { range } from 'lodash';
+import range from 'lodash/range';
 import React from 'react';
 import PageControl from 'components/common/Pagination/PageControl';
 import useSearch from 'lib/hooks/useSearch';

+ 8 - 0
kafka-ui-react-app/src/lib/hooks/api/useClusters.ts

@@ -0,0 +1,8 @@
+import { clustersApiClient } from 'lib/api';
+import { useQuery } from 'react-query';
+
+export default function useClusters() {
+  return useQuery(['clusters'], () => clustersApiClient.getClusters(), {
+    suspense: true,
+  });
+}

+ 1 - 1
kafka-ui-react-app/src/lib/hooks/useDataSaver.ts

@@ -1,4 +1,4 @@
-import { isObject } from 'lodash';
+import isObject from 'lodash/isObject';
 import { alertAdded, alertDissmissed } from 'redux/reducers/alerts/alertsSlice';
 import { useAppDispatch } from 'lib/hooks/redux';
 

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

@@ -6,7 +6,7 @@ import {
   PayloadAction,
 } from '@reduxjs/toolkit';
 import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers';
-import { now } from 'lodash';
+import now from 'lodash/now';
 import { Alert, RootState, ServerResponse } from 'redux/interfaces';
 
 const alertsAdapter = createEntityAdapter<Alert>({

+ 0 - 55
kafka-ui-react-app/src/redux/reducers/clusters/__test__/reducer.spec.ts

@@ -1,55 +0,0 @@
-import fetchMock from 'fetch-mock-jest';
-import reducer, { fetchClusters } from 'redux/reducers/clusters/clustersSlice';
-import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
-
-import { clustersPayload } from './fixtures';
-
-const store = mockStoreCreator;
-
-describe('Clusters Slice', () => {
-  describe('Reducer', () => {
-    it('returns the initial state', () => {
-      expect(reducer(undefined, { type: fetchClusters.pending })).toEqual([]);
-    });
-
-    it('reacts on fetchClusters.fulfilled and returns payload', () => {
-      expect(
-        reducer([], {
-          type: fetchClusters.fulfilled,
-          payload: clustersPayload,
-        })
-      ).toEqual(clustersPayload);
-    });
-  });
-
-  describe('thunks', () => {
-    afterEach(() => {
-      fetchMock.restore();
-      store.clearActions();
-    });
-
-    describe('fetchClusters', () => {
-      it('creates fetchClusters.fulfilled when fetched clusters', async () => {
-        fetchMock.getOnce('/api/clusters', clustersPayload);
-        await store.dispatch(fetchClusters());
-        expect(
-          store.getActions().map(({ type, payload }) => ({ type, payload }))
-        ).toEqual([
-          { type: fetchClusters.pending.type },
-          { type: fetchClusters.fulfilled.type, payload: clustersPayload },
-        ]);
-      });
-
-      it('creates fetchClusters.rejected when fetched clusters', async () => {
-        fetchMock.getOnce('/api/clusters', 422);
-        await store.dispatch(fetchClusters());
-        expect(
-          store.getActions().map(({ type, payload }) => ({ type, payload }))
-        ).toEqual([
-          { type: fetchClusters.pending.type },
-          { type: fetchClusters.rejected.type },
-        ]);
-      });
-    });
-  });
-});

+ 0 - 60
kafka-ui-react-app/src/redux/reducers/clusters/__test__/selectors.spec.ts

@@ -1,60 +0,0 @@
-import { store } from 'redux/store';
-import {
-  fetchClusters,
-  getAreClustersFulfilled,
-  getClusterList,
-  getOnlineClusters,
-  getOfflineClusters,
-} from 'redux/reducers/clusters/clustersSlice';
-
-import {
-  clustersPayload,
-  offlineClusterPayload,
-  onlineClusterPayload,
-} from './fixtures';
-
-describe('Clusters selectors', () => {
-  describe('Initial State', () => {
-    it('returns fetch status', () => {
-      expect(getAreClustersFulfilled(store.getState())).toBeFalsy();
-    });
-
-    it('returns cluster list', () => {
-      expect(getClusterList(store.getState())).toEqual([]);
-    });
-
-    it('returns online cluster list', () => {
-      expect(getOnlineClusters(store.getState())).toEqual([]);
-    });
-
-    it('returns offline cluster list', () => {
-      expect(getOfflineClusters(store.getState())).toEqual([]);
-    });
-  });
-
-  describe('state', () => {
-    beforeAll(() => {
-      store.dispatch(fetchClusters.fulfilled(clustersPayload, '1234'));
-    });
-
-    it('returns fetch status', () => {
-      expect(getAreClustersFulfilled(store.getState())).toBeTruthy();
-    });
-
-    it('returns cluster list', () => {
-      expect(getClusterList(store.getState())).toEqual(clustersPayload);
-    });
-
-    it('returns online cluster list', () => {
-      expect(getOnlineClusters(store.getState())).toEqual([
-        onlineClusterPayload,
-      ]);
-    });
-
-    it('returns offline cluster list', () => {
-      expect(getOfflineClusters(store.getState())).toEqual([
-        offlineClusterPayload,
-      ]);
-    });
-  });
-});

+ 0 - 61
kafka-ui-react-app/src/redux/reducers/clusters/clustersSlice.ts

@@ -1,61 +0,0 @@
-import {
-  createAsyncThunk,
-  createSlice,
-  createSelector,
-} from '@reduxjs/toolkit';
-import { Cluster, ServerStatus, ClusterFeaturesEnum } from 'generated-sources';
-import { clustersApiClient } from 'lib/api';
-import { AsyncRequestStatus } from 'lib/constants';
-import { RootState } from 'redux/interfaces';
-import { createFetchingSelector } from 'redux/reducers/loader/selectors';
-
-export const fetchClusters = createAsyncThunk(
-  'clusters/fetchClusters',
-  async () => {
-    const clusters: Cluster[] = await clustersApiClient.getClusters();
-    return clusters;
-  }
-);
-
-export const initialState: Cluster[] = [];
-export const clustersSlice = createSlice({
-  name: 'clusters',
-  initialState,
-  reducers: {},
-  extraReducers: (builder) => {
-    builder.addCase(fetchClusters.fulfilled, (_, { payload }) => payload);
-  },
-});
-
-const clustersState = ({ clusters }: RootState): Cluster[] => clusters;
-const getClusterListFetchingStatus = createFetchingSelector(
-  'clusters/fetchClusters'
-);
-export const getAreClustersFulfilled = createSelector(
-  getClusterListFetchingStatus,
-  (status) => status === AsyncRequestStatus.fulfilled
-);
-export const getClusterList = createSelector(
-  clustersState,
-  (clusters) => clusters
-);
-export const getOnlineClusters = createSelector(getClusterList, (clusters) =>
-  clusters.filter(({ status }) => status === ServerStatus.ONLINE)
-);
-export const getOfflineClusters = createSelector(getClusterList, (clusters) =>
-  clusters.filter(({ status }) => status === ServerStatus.OFFLINE)
-);
-export const getClustersReadonlyStatus = (clusterName: string) =>
-  createSelector(
-    getClusterList,
-    (clusters): boolean =>
-      clusters.find(({ name }) => name === clusterName)?.readOnly || false
-  );
-export const getClustersFeatures = (clusterName: string) =>
-  createSelector(
-    getClusterList,
-    (clusters): ClusterFeaturesEnum[] =>
-      clusters.find(({ name }) => name === clusterName)?.features || []
-  );
-
-export default clustersSlice.reducer;

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

@@ -6,7 +6,7 @@ import {
   ConnectorState,
   FullConnectorInfo,
 } from 'generated-sources';
-import { sortBy } from 'lodash';
+import sortBy from 'lodash/sortBy';
 import { AsyncRequestStatus } from 'lib/constants';
 
 import {

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

@@ -1,5 +1,4 @@
 import { combineReducers } from '@reduxjs/toolkit';
-import clusters from 'redux/reducers/clusters/clustersSlice';
 import loader from 'redux/reducers/loader/loaderSlice';
 import alerts from 'redux/reducers/alerts/alertsSlice';
 import schemas from 'redux/reducers/schemas/schemasSlice';
@@ -14,7 +13,6 @@ export default combineReducers({
   alerts,
   topics,
   topicMessages,
-  clusters,
   consumerGroups,
   schemas,
   connect,

+ 0 - 1
kafka-ui-react-app/vite.config.ts

@@ -22,7 +22,6 @@ export default defineConfig(({ mode }) => {
               'styled-components',
               'react-ace',
             ],
-            lodash: ['lodash'],
           },
         },
       },