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

This commit is contained in:
Oleg Shur 2022-06-28 15:15:12 +03:00 committed by GitHub
parent 0efbd130b9
commit a4046d46ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 274 additions and 572 deletions

View file

@ -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 />

View file

@ -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={

View file

@ -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();
});
});
});

View file

@ -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[] = [

View file

@ -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';

View file

@ -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';

View file

@ -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>
))}
</>
);
};

View file

@ -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);

View file

@ -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);

View file

@ -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();
});
});

View file

@ -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];

View file

@ -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>
</>
);

View file

@ -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();
});
});

View file

@ -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';

View file

@ -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;

View file

@ -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) => (

View file

@ -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();

View file

@ -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';

View file

@ -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) => {

View file

@ -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';

View file

@ -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,

View file

@ -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();
});
});

View file

@ -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';

View 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,
});
}

View file

@ -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';

View file

@ -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>({

View file

@ -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 },
]);
});
});
});
});

View file

@ -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,
]);
});
});
});

View file

@ -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;

View file

@ -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 {

View file

@ -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,

View file

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