Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added clusters overview grid for Kafka Connect #542

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/compose/kafbat-ui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ services:
kafka-init-topics:
image: confluentinc/cp-kafka:7.2.1
volumes:
- ./data/message.json:/data/message.json
- ./data/message.json:/data/message.json
depends_on:
- kafka1
command: "bash -c 'echo Waiting for Kafka to be ready... && \
Expand Down
2 changes: 1 addition & 1 deletion frontend/.env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
#
#
135 changes: 135 additions & 0 deletions frontend/src/components/Connect/List/ClustersList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { useMemo, useEffect, useState } from 'react';
import useAppParams from 'lib/hooks/useAppParams';
import { ClusterNameRoute } from 'lib/paths';
import Table from 'components/common/NewTable';
import { Connect, ConnectorState } from 'generated-sources';
import {
useConnects,
useConnectors,
fetchVersion,
} from 'lib/hooks/api/kafkaConnect';
import { ColumnDef, Row } from '@tanstack/react-table';
import toast from 'react-hot-toast';

interface ClusterWithStats extends Connect {
runningConnectorsCount: number;
totalConnectorsCount: number;
runningTasksCount: number;
totalTasksCount: number;
}

const ConnectorsCell: React.FC<{ row: Row<ClusterWithStats> }> = ({ row }) => {
const { runningConnectorsCount, totalConnectorsCount } = row.original;

return (
<span>
{totalConnectorsCount === 0
? runningConnectorsCount
: `${runningConnectorsCount} of ${totalConnectorsCount}`}
</span>
);
};

const TasksCell: React.FC<{ row: Row<ClusterWithStats> }> = ({ row }) => {
const { runningTasksCount, totalTasksCount } = row.original;

return (
<span>
{totalTasksCount === 0
? runningTasksCount
: `${runningTasksCount} of ${totalTasksCount}`}
</span>
);
};

const ClusterVersionCell: React.FC<{ row: Row<ClusterWithStats> }> = ({
row,
}) => {
const { name, address } = row.original;
const [version, setVersion] = useState<string>('Loading...');

useEffect(() => {
const fetchClusterVersion = async () => {
if (address) {
const fetchedVersion = await fetchVersion(name, address);
setVersion(fetchedVersion);
} else {
toast.error(
`Failed to retrieve the version from cluster ${name}, address is missing.`
);
setVersion('Unknown');
}
};
fetchClusterVersion().then();
}, [address]);

return <span>{version}</span>;
};

const ClustersList: React.FC = () => {
const { clusterName } = useAppParams<ClusterNameRoute>();
const { data: connects = [] } = useConnects(clusterName);
const { data: connectorsMetrics = [] } = useConnectors(clusterName);

const [clustersData, setClustersData] = useState<ClusterWithStats[]>([]);

useEffect(() => {
const clustersWithStats = connects.map((connect) => {
const relatedConnectors = connectorsMetrics.filter(
(connector) => connector.connect === connect.name
);

const runningConnectorsCount = relatedConnectors.filter(
(connector) => connector.status.state === ConnectorState.RUNNING
).length;

const totalConnectorsCount = relatedConnectors.length;

const totalTasksCount = relatedConnectors.reduce(
(sum, connector) => sum + (connector.tasksCount || 0),
0
);

const runningTasksCount =
totalTasksCount -
relatedConnectors.reduce(
(sum, connector) => sum + (connector.failedTasksCount || 0),
0
);

return {
...connect,
runningConnectorsCount,
totalConnectorsCount,
runningTasksCount,
totalTasksCount,
};
});

if (JSON.stringify(clustersData) !== JSON.stringify(clustersWithStats)) {
setClustersData(clustersWithStats);
}
}, [connects, connectorsMetrics]);

const columns = useMemo<ColumnDef<ClusterWithStats>[]>(
() => [
{ header: 'Cluster Name', accessorKey: 'name' },
{ header: 'Version', cell: ClusterVersionCell },
{ header: 'Connectors', cell: ConnectorsCell },
{ header: 'Running Tasks', cell: TasksCell },
],
[]
);

return (
<Table
data={clustersData}
columns={columns}
enableSorting
emptyMessage="No clusters found"
setRowId={(originalRow) => originalRow.name}
/>
);
};

export default ClustersList;
39 changes: 39 additions & 0 deletions frontend/src/components/Connect/List/ListPage.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import styled from 'styled-components';

export const Navbar = styled.nav`
display: flex;
border-bottom: 1px ${({ theme }) => theme.primaryTab.borderColor.nav} solid;
height: ${({ theme }) => theme.primaryTab.height};
`;

export const Tab = styled.a<{ isActive?: boolean }>`
height: 40px;
min-width: 96px;
padding: 0 16px;
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
font-size: 14px;
white-space: nowrap;
color: ${({ theme, isActive }) =>
isActive ? theme.primaryTab.color.active : theme.primaryTab.color.normal};
border-bottom: 1px
${({ theme, isActive }) =>
isActive
? theme.primaryTab.borderColor.active
: theme.default.transparentColor}
solid;
cursor: ${({ isActive }) => (isActive ? 'default' : 'pointer')};
&:hover {
color: ${({ theme, isActive }) =>
isActive ? theme.primaryTab.color.active : theme.primaryTab.color.hover};
border-bottom: 1px
${({ theme, isActive }) =>
isActive
? theme.primaryTab.borderColor.active
: theme.default.transparentColor}
solid;
}
`;
35 changes: 28 additions & 7 deletions frontend/src/components/Connect/List/ListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import React, { Suspense } from 'react';
import React, { useState, Suspense } from 'react';
import useAppParams from 'lib/hooks/useAppParams';
import { ClusterNameRoute, clusterConnectorNewRelativePath } from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext';
import Search from 'components/common/Search/Search';
import * as Metrics from 'components/common/Metrics';
import PageHeading from 'components/common/PageHeading/PageHeading';
import Tooltip from 'components/common/Tooltip/Tooltip';
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
import PageLoader from 'components/common/PageLoader/PageLoader';
import { ConnectorState, Action, ResourceType } from 'generated-sources';
import { useConnectors, useConnects } from 'lib/hooks/api/kafkaConnect';
import { ActionButton } from 'components/common/ActionComponent';

import * as S from './ListPage.styled';
import List from './List';
import ClustersList from './ClustersList';

const ListPage: React.FC = () => {
const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName } = useAppParams<ClusterNameRoute>();

const { data: connects = [] } = useConnects(clusterName);

// Fetches all connectors from the API, without search criteria. Used to display general metrics.
const { data: connectorsMetrics, isLoading } = useConnectors(clusterName);
const [viewType, setViewType] = useState<string>('connectors');

const numberOfFailedConnectors = connectorsMetrics?.filter(
({ status: { state } }) => state === ConnectorState.FAILED
Expand Down Expand Up @@ -79,13 +81,32 @@ const ListPage: React.FC = () => {
>
{numberOfFailedTasks ?? '-'}
</Metrics.Indicator>
<Metrics.Indicator
label="Clusters"
title="Total number of clusters"
fetching={isLoading}
>
{connects?.length || '-'}
</Metrics.Indicator>
</Metrics.Section>
</Metrics.Wrapper>
<ControlPanelWrapper hasInput>
<Search placeholder="Search by Connect Name, Status or Type" />
</ControlPanelWrapper>
<S.Navbar>
<S.Tab
isActive={viewType === 'connectors'}
onClick={() => setViewType('connectors')}
>
Connectors
</S.Tab>
<S.Tab
isActive={viewType === 'clusters'}
onClick={() => setViewType('clusters')}
>
Clusters
</S.Tab>
</S.Navbar>
<Suspense fallback={<PageLoader />}>
<List />
{viewType === 'connectors' && <List />}
{viewType === 'clusters' && <ClustersList />}
</Suspense>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,6 @@ describe('Connectors List Page', () => {
});
});

it('renders search input', async () => {
await renderComponent();
expect(
screen.getByPlaceholderText('Search by Connect Name, Status or Type')
).toBeInTheDocument();
});

it('renders list', async () => {
await renderComponent();
expect(screen.getByText('Connectors List')).toBeInTheDocument();
Expand All @@ -89,7 +82,7 @@ describe('Connectors List Page', () => {
await renderComponent();
const metrics = screen.getByRole('group');
expect(metrics).toBeInTheDocument();
expect(within(metrics).getAllByText('progressbar').length).toEqual(3);
expect(within(metrics).getAllByText('progressbar').length).toEqual(4);
});

it('renders indicators for empty list of connectors', async () => {
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/lib/hooks/api/kafkaConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { kafkaConnectApiClient as api } from 'lib/api';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ClusterName } from 'lib/interfaces/cluster';
import { showSuccessAlert } from 'lib/errorHandling';
import toast from 'react-hot-toast';

interface UseConnectorProps {
clusterName: ClusterName;
Expand Down Expand Up @@ -161,3 +162,23 @@ export function useDeleteConnector(props: UseConnectorProps) {
onSuccess: () => client.invalidateQueries(connectorsKey(props.clusterName)),
});
}

export async function fetchVersion(name: string, address: string) {
try {
const response = await fetch(`${address}/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

const data = await response.json();
return data.version || 'Unknown';
} catch (e) {
const error = e as Error;
toast.error(
`Failed to retrieve the version from cluster ${name}. Error: ${error?.message}. Please verify your CORS configuration.`
);
return 'Unknown';
}
}
Loading