chore: migrate clusters from toolkit to react-query (#2214)
This commit is contained in:
parent
0efbd130b9
commit
a4046d46ef
32 changed files with 274 additions and 572 deletions
|
@ -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,7 +89,6 @@ const App: React.FC = () => {
|
|||
aria-hidden="true"
|
||||
aria-label="Overlay"
|
||||
/>
|
||||
{areClustersFulfilled ? (
|
||||
<Routes>
|
||||
{['/', '/ui', '/ui/clusters'].map((path) => (
|
||||
<Route
|
||||
|
@ -117,9 +102,6 @@ const App: React.FC = () => {
|
|||
element={<ClusterPage />}
|
||||
/>
|
||||
</Routes>
|
||||
) : (
|
||||
<PageLoader />
|
||||
)}
|
||||
</S.Container>
|
||||
<S.AlertsContainer role="toolbar">
|
||||
<Alerts />
|
||||
|
|
|
@ -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 { data } = useClusters();
|
||||
const contextValue = React.useMemo(() => {
|
||||
const cluster = data?.find(({ name }) => name === clusterName);
|
||||
const features = cluster?.features || [];
|
||||
|
||||
const hasKafkaConnectConfigured = features.includes(
|
||||
return {
|
||||
isReadOnly: cluster?.readOnly || false,
|
||||
hasKafkaConnectConfigured: features.includes(
|
||||
ClusterFeaturesEnum.KAFKA_CONNECT
|
||||
);
|
||||
const hasSchemaRegistryConfigured = features.includes(
|
||||
),
|
||||
hasSchemaRegistryConfigured: features.includes(
|
||||
ClusterFeaturesEnum.SCHEMA_REGISTRY
|
||||
);
|
||||
const isTopicDeletionAllowed = features.includes(
|
||||
),
|
||||
isTopicDeletionAllowed: features.includes(
|
||||
ClusterFeaturesEnum.TOPIC_DELETION
|
||||
);
|
||||
const hasKsqlDbConfigured = features.includes(ClusterFeaturesEnum.KSQL_DB);
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
isReadOnly,
|
||||
hasKafkaConnectConfigured,
|
||||
hasSchemaRegistryConfigured,
|
||||
isTopicDeletionAllowed,
|
||||
}),
|
||||
[
|
||||
hasKafkaConnectConfigured,
|
||||
hasSchemaRegistryConfigured,
|
||||
isReadOnly,
|
||||
isTopicDeletionAllowed,
|
||||
]
|
||||
);
|
||||
),
|
||||
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={
|
||||
|
|
|
@ -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) => {
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
const renderComponent = async (pathname: string, payload: Cluster[] = []) => {
|
||||
const mock = fetchMock.get('/api/clusters', payload);
|
||||
await act(() => {
|
||||
render(
|
||||
<WithRoute path={`${clusterPath()}/*`}>
|
||||
<Cluster />
|
||||
<ClusterComponent />
|
||||
</WithRoute>,
|
||||
{ initialEntries: [pathname], store }
|
||||
{ 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(
|
||||
[
|
||||
const itCorrectlyHandlesConfiguredSchema = (
|
||||
feature: ClusterFeaturesEnum,
|
||||
text: string,
|
||||
path: string
|
||||
) => {
|
||||
it(`renders Schemas if ${feature} is configured`, async () => {
|
||||
await renderComponent(path, [
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [],
|
||||
features: [feature],
|
||||
},
|
||||
],
|
||||
'123'
|
||||
)
|
||||
);
|
||||
await act(() => renderComponent(clusterSchemasPath('second')));
|
||||
expect(
|
||||
screen.queryByText(CLusterCompText.Schemas)
|
||||
).not.toBeInTheDocument();
|
||||
]);
|
||||
expect(screen.getByText(text)).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(`does not render Schemas if ${feature} is not configured`, async () => {
|
||||
await renderComponent(path, [
|
||||
{ ...onlineClusterPayload, features: [] },
|
||||
]);
|
||||
expect(screen.queryByText(text)).not.toBeInTheDocument();
|
||||
});
|
||||
it('renders Connect if KAFKA_CONNECT is configured', async () => {
|
||||
store.dispatch(
|
||||
fetchClusters.fulfilled(
|
||||
[
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [ClusterFeaturesEnum.KAFKA_CONNECT],
|
||||
},
|
||||
],
|
||||
'requestId'
|
||||
)
|
||||
};
|
||||
|
||||
itCorrectlyHandlesConfiguredSchema(
|
||||
ClusterFeaturesEnum.SCHEMA_REGISTRY,
|
||||
CLusterCompText.Schemas,
|
||||
clusterSchemasPath(onlineClusterPayload.name)
|
||||
);
|
||||
await act(() =>
|
||||
renderComponent(clusterConnectsPath(onlineClusterPayload.name))
|
||||
itCorrectlyHandlesConfiguredSchema(
|
||||
ClusterFeaturesEnum.KAFKA_CONNECT,
|
||||
CLusterCompText.Connect,
|
||||
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'
|
||||
)
|
||||
itCorrectlyHandlesConfiguredSchema(
|
||||
ClusterFeaturesEnum.KAFKA_CONNECT,
|
||||
CLusterCompText.Connect,
|
||||
clusterConnectorsPath(onlineClusterPayload.name)
|
||||
);
|
||||
await act(() =>
|
||||
renderComponent(clusterKsqlDbPath(onlineClusterPayload.name))
|
||||
itCorrectlyHandlesConfiguredSchema(
|
||||
ClusterFeaturesEnum.KSQL_DB,
|
||||
CLusterCompText.KsqlDb,
|
||||
clusterKsqlDbPath(onlineClusterPayload.name)
|
||||
);
|
||||
expect(screen.getByText(CLusterCompText.KsqlDb)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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[] = [
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,8 +51,7 @@ const ClustersWidget: React.FC<Props> = ({
|
|||
/>
|
||||
<label>Only offline clusters</label>
|
||||
</S.SwitchWrapper>
|
||||
{clusterList.map((chunkItem) => (
|
||||
<Table key={chunkItem.map(({ name }) => name).join('-')} isFullwidth>
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Cluster name" />
|
||||
|
@ -70,7 +64,7 @@ const ClustersWidget: React.FC<Props> = ({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chunkItem.map((cluster) => (
|
||||
{config.list.map((cluster) => (
|
||||
<tr key={cluster.name}>
|
||||
<S.TableCell maxWidth="99px" width="350">
|
||||
{cluster.readOnly && <Tag color="blue">readonly</Tag>}{' '}
|
||||
|
@ -96,7 +90,6 @@ const ClustersWidget: React.FC<Props> = ({
|
|||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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 { offlineCluster, onlineCluster, clusters } from './fixtures';
|
||||
|
||||
const setupComponent = () =>
|
||||
render(
|
||||
<ClustersWidget
|
||||
clusters={clusters}
|
||||
onlineClusters={[onlineCluster]}
|
||||
offlineClusters={[offlineCluster]}
|
||||
/>
|
||||
);
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { clustersPayload } from 'components/Cluster/__tests__/fixtures';
|
||||
|
||||
describe('ClustersWidget', () => {
|
||||
beforeEach(() => setupComponent());
|
||||
afterEach(() => fetchMock.restore());
|
||||
|
||||
beforeEach(async () => {
|
||||
const mock = fetchMock.get('/api/clusters', clustersPayload);
|
||||
|
||||
await act(() => {
|
||||
render(<ClustersWidget />);
|
||||
});
|
||||
await waitFor(() => expect(mock.called()).toBeTruthy());
|
||||
});
|
||||
|
||||
it('renders clusterWidget list', () => {
|
||||
expect(screen.getAllByRole('row').length).toBe(3);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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];
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
||||
if (!query.isSuccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Nav: React.FC<Props> = ({ areClustersFulfilled, clusters }) => (
|
||||
return (
|
||||
<aside aria-label="Sidebar Menu">
|
||||
<S.List>
|
||||
<ClusterMenuItem to="/" title="Dashboard" isTopLevel />
|
||||
</S.List>
|
||||
|
||||
{areClustersFulfilled &&
|
||||
clusters.map((cluster) => (
|
||||
{query.data.map((cluster) => (
|
||||
<ClusterMenu
|
||||
cluster={cluster}
|
||||
key={cluster.name}
|
||||
singleMode={clusters.length === 1}
|
||||
singleMode={query.data.length === 1}
|
||||
/>
|
||||
))}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,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) => {
|
||||
|
|
|
@ -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,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,
|
||||
|
|
|
@ -1,31 +1,25 @@
|
|||
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).getByText('UI for Apache Kafka')).toBeInTheDocument();
|
||||
expect(within(header).getAllByRole('separator').length).toEqual(3);
|
||||
expect(
|
||||
within(header).getByRole('button', burgerButtonOptions)
|
||||
|
@ -34,41 +28,22 @@ describe('App', () => {
|
|||
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 burger = within(screen.getByLabelText('Page Header')).getByRole(
|
||||
'button',
|
||||
burgerButtonOptions
|
||||
);
|
||||
const overlay = screen.getByLabelText('Overlay');
|
||||
expect(sidebar).toBeInTheDocument();
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with clusters list fetched', () => {
|
||||
it('shows Cluster list', async () => {
|
||||
const mock = fetchMock.getOnce('/api/clusters', clustersPayload);
|
||||
await act(() => {
|
||||
render(<App />, {
|
||||
initialEntries: ['/'],
|
||||
});
|
||||
});
|
||||
|
||||
expect(mock.called()).toBeTruthy();
|
||||
|
||||
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,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
kafka-ui-react-app/src/lib/hooks/api/useClusters.ts
Normal file
8
kafka-ui-react-app/src/lib/hooks/api/useClusters.ts
Normal file
|
@ -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,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';
|
||||
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -22,7 +22,6 @@ export default defineConfig(({ mode }) => {
|
|||
'styled-components',
|
||||
'react-ace',
|
||||
],
|
||||
lodash: ['lodash'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue