diff --git a/e2e-tests/src/main/java/io/kafbat/ui/screens/connectors/KafkaConnectList.java b/e2e-tests/src/main/java/io/kafbat/ui/screens/connectors/KafkaConnectList.java
index 5045e99d0..6dddae78b 100644
--- a/e2e-tests/src/main/java/io/kafbat/ui/screens/connectors/KafkaConnectList.java
+++ b/e2e-tests/src/main/java/io/kafbat/ui/screens/connectors/KafkaConnectList.java
@@ -13,13 +13,21 @@
public class KafkaConnectList extends BasePage {
protected SelenideElement createConnectorBtn = $x("//button[contains(text(),'Create Connector')]");
+ protected SelenideElement connectorsTab = $x("//a[contains(text(),'Connectors')]");
public KafkaConnectList() {
tableElementNameLocator = "//tbody//td[contains(text(),'%s')]";
}
+ @Step
+ public KafkaConnectList clickConnectorsTab() {
+ WebUtil.clickByJavaScript(connectorsTab);
+ return this;
+ }
+
@Step
public KafkaConnectList waitUntilScreenReady() {
+ clickConnectorsTab();
waitUntilSpinnerDisappear();
getPageTitleFromHeader(KAFKA_CONNECT).shouldBe(Condition.visible);
return this;
diff --git a/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/enums/MenuItem.java b/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/enums/MenuItem.java
index 283d428a1..caa6025eb 100644
--- a/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/enums/MenuItem.java
+++ b/e2e-tests/src/main/java/io/kafbat/ui/screens/panels/enums/MenuItem.java
@@ -10,7 +10,7 @@ public enum MenuItem {
TOPICS("Topics", "Topics"),
CONSUMERS("Consumers", "Consumers"),
SCHEMA_REGISTRY("Schema Registry", "Schema Registry"),
- KAFKA_CONNECT("Kafka Connect", "Connectors"),
+ KAFKA_CONNECT("Kafka Connect", "Kafka Connect"),
KSQL_DB("KSQL DB", "KSQL DB");
private final String naviTitle;
diff --git a/e2e-tests/src/main/java/io/kafbat/ui/variables/Url.java b/e2e-tests/src/main/java/io/kafbat/ui/variables/Url.java
index 93eee2676..29591b882 100644
--- a/e2e-tests/src/main/java/io/kafbat/ui/variables/Url.java
+++ b/e2e-tests/src/main/java/io/kafbat/ui/variables/Url.java
@@ -6,6 +6,6 @@ public interface Url {
String TOPICS_LIST_URL = "http://%s:8080/ui/clusters/local/all-topics";
String CONSUMERS_LIST_URL = "http://%s:8080/ui/clusters/local/consumer-groups";
String SCHEMA_REGISTRY_LIST_URL = "http://%s:8080/ui/clusters/local/schemas";
- String KAFKA_CONNECT_LIST_URL = "http://%s:8080/ui/clusters/local/connectors";
+ String KAFKA_CONNECT_LIST_URL = "http://%s:8080/ui/clusters/local/kafka-connect/connectors";
String KSQL_DB_LIST_URL = "http://%s:8080/ui/clusters/local/ksqldb/tables";
}
diff --git a/frontend/src/components/ClusterPage/ClusterPage.tsx b/frontend/src/components/ClusterPage/ClusterPage.tsx
index 29d2015f6..a0cc13e13 100644
--- a/frontend/src/components/ClusterPage/ClusterPage.tsx
+++ b/frontend/src/components/ClusterPage/ClusterPage.tsx
@@ -4,8 +4,6 @@ import useAppParams from 'lib/hooks/useAppParams';
import { ClusterFeaturesEnum } from 'generated-sources';
import {
clusterBrokerRelativePath,
- clusterConnectorsRelativePath,
- clusterConnectsRelativePath,
clusterConsumerGroupsRelativePath,
clusterKsqlDbRelativePath,
ClusterNameRoute,
@@ -14,16 +12,23 @@ import {
clusterConfigRelativePath,
getNonExactPath,
clusterAclRelativePath,
+ kafkaConnectRelativePath,
+ clusterConnectorNewRelativePath,
+ clusterConnectConnectorRelativePath,
} from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext';
import PageLoader from 'components/common/PageLoader/PageLoader';
import { useClusters } from 'lib/hooks/api/clusters';
import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';
+import New from 'components/Connect/New/New';
+import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';
+import DetailsPage from 'components/Connect/Details/DetailsPage';
const Brokers = React.lazy(() => import('components/Brokers/Brokers'));
const Topics = React.lazy(() => import('components/Topics/Topics'));
const Schemas = React.lazy(() => import('components/Schemas/Schemas'));
-const Connect = React.lazy(() => import('components/Connect/Connect'));
+const KafkaConnect = React.lazy(() => import('components/Connect/Connect'));
+
const KsqlDb = React.lazy(() => import('components/KsqlDb/KsqlDb'));
const ClusterConfigPage = React.lazy(
() => import('components/ClusterPage/ClusterConfigPage')
@@ -82,16 +87,23 @@ const ClusterPage: React.FC = () => {
element={}
/>
)}
+ {contextValue.hasKafkaConnectConfigured && (
+ } />
+ )}
{contextValue.hasKafkaConnectConfigured && (
}
+ path={getNonExactPath(clusterConnectConnectorRelativePath)}
+ element={
+
+
+
+ }
/>
)}
{contextValue.hasKafkaConnectConfigured && (
}
+ path={getNonExactPath(kafkaConnectRelativePath)}
+ element={}
/>
)}
{contextValue.hasKsqlDbConfigured && (
diff --git a/frontend/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx b/frontend/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx
index b66fd0a0b..238c13155 100644
--- a/frontend/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx
+++ b/frontend/src/components/ClusterPage/__tests__/ClusterPage.spec.tsx
@@ -6,12 +6,12 @@ import { render, WithRoute } from 'lib/testHelpers';
import {
clusterBrokersPath,
clusterConnectorsPath,
- clusterConnectsPath,
clusterConsumerGroupsPath,
clusterKsqlDbPath,
clusterPath,
clusterSchemasPath,
clusterTopicsPath,
+ kafkaConnectPath,
} from 'lib/paths';
import { useClusters } from 'lib/hooks/api/clusters';
import { onlineClusterPayload } from 'lib/fixtures/clusters';
@@ -19,7 +19,7 @@ import { onlineClusterPayload } from 'lib/fixtures/clusters';
const CLusterCompText = {
Topics: 'Topics',
Schemas: 'Schemas',
- Connect: 'Connect',
+ Connect: 'Kafka Connect',
Brokers: 'Brokers',
ConsumerGroups: 'ConsumerGroups',
KsqlDb: 'KsqlDb',
@@ -111,7 +111,7 @@ describe('ClusterPage', () => {
itCorrectlyHandlesConfiguredSchema(
ClusterFeaturesEnum.KAFKA_CONNECT,
CLusterCompText.Connect,
- clusterConnectsPath(onlineClusterPayload.name)
+ kafkaConnectPath(onlineClusterPayload.name)
);
itCorrectlyHandlesConfiguredSchema(
ClusterFeaturesEnum.KAFKA_CONNECT,
diff --git a/frontend/src/components/Connect/Clusters/Clusters.tsx b/frontend/src/components/Connect/Clusters/Clusters.tsx
new file mode 100644
index 000000000..2b98ea953
--- /dev/null
+++ b/frontend/src/components/Connect/Clusters/Clusters.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { useConnects } from 'lib/hooks/api/kafkaConnect';
+import useAppParams from 'lib/hooks/useAppParams';
+import { ClusterNameRoute } from 'lib/paths';
+
+import ClustersStatistics from './ui/Statistics/Statistics';
+import List from './ui/List/List';
+
+const KafkaConnectClustersPage = () => {
+ const { clusterName } = useAppParams();
+ const { data: connects, isLoading } = useConnects(clusterName, true);
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default KafkaConnectClustersPage;
diff --git a/frontend/src/components/Connect/Clusters/ui/List/Cells/ConnectorsCell.tsx b/frontend/src/components/Connect/Clusters/ui/List/Cells/ConnectorsCell.tsx
new file mode 100644
index 000000000..2319cc07d
--- /dev/null
+++ b/frontend/src/components/Connect/Clusters/ui/List/Cells/ConnectorsCell.tsx
@@ -0,0 +1,26 @@
+import AlertBadge from 'components/common/AlertBadge/AlertBadge';
+import { Connect } from 'generated-sources';
+import React from 'react';
+
+type Props = { connect: Connect };
+const ConnectorsCell = ({ connect }: Props) => {
+ const count = connect.connectorsCount ?? 0;
+ const failedCount = connect.failedConnectorsCount ?? 0;
+ const text = `${count - failedCount}/${count}`;
+
+ if (count === 0) {
+ return null;
+ }
+
+ if (failedCount > 0) {
+ return (
+
+
+
+
+ );
+ }
+
+ return text;
+};
+export default ConnectorsCell;
diff --git a/frontend/src/components/Connect/Clusters/ui/List/Cells/NameCell.tsx b/frontend/src/components/Connect/Clusters/ui/List/Cells/NameCell.tsx
new file mode 100644
index 000000000..dece56545
--- /dev/null
+++ b/frontend/src/components/Connect/Clusters/ui/List/Cells/NameCell.tsx
@@ -0,0 +1,9 @@
+import { CellContext } from '@tanstack/react-table';
+import { Connect } from 'generated-sources';
+import React from 'react';
+
+type Props = CellContext;
+const NameCell = ({ getValue }: Props) => {
+ return {getValue()}
;
+};
+export default NameCell;
diff --git a/frontend/src/components/Connect/Clusters/ui/List/Cells/TasksCell.tsx b/frontend/src/components/Connect/Clusters/ui/List/Cells/TasksCell.tsx
new file mode 100644
index 000000000..041237e0a
--- /dev/null
+++ b/frontend/src/components/Connect/Clusters/ui/List/Cells/TasksCell.tsx
@@ -0,0 +1,26 @@
+import AlertBadge from 'components/common/AlertBadge/AlertBadge';
+import { Connect } from 'generated-sources';
+import React from 'react';
+
+type Props = { connect: Connect };
+const TasksCell = ({ connect }: Props) => {
+ const count = connect.tasksCount ?? 0;
+ const failedCount = connect.failedTasksCount ?? 0;
+ const text = `${count - failedCount}/${count}`;
+
+ if (!count) {
+ return null;
+ }
+
+ if (failedCount > 0) {
+ return (
+
+
+
+
+ );
+ }
+
+ return {text}
;
+};
+export default TasksCell;
diff --git a/frontend/src/components/Connect/Clusters/ui/List/List.tsx b/frontend/src/components/Connect/Clusters/ui/List/List.tsx
new file mode 100644
index 000000000..f0cd4e693
--- /dev/null
+++ b/frontend/src/components/Connect/Clusters/ui/List/List.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Connect } from 'generated-sources';
+import Table from 'components/common/NewTable';
+import useAppParams from 'lib/hooks/useAppParams';
+import { ClusterName } from 'lib/interfaces/cluster';
+import { useNavigate } from 'react-router-dom';
+import { clusterConnectorsPath } from 'lib/paths';
+import { createColumnHelper } from '@tanstack/react-table';
+
+import ConnectorsCell from './Cells/ConnectorsCell';
+import NameCell from './Cells/NameCell';
+import TasksCell from './Cells/TasksCell';
+
+const helper = createColumnHelper();
+export const columns = [
+ helper.accessor('name', { cell: NameCell, size: 600 }),
+ helper.accessor('version', {
+ header: 'Version',
+ cell: ({ getValue }) => getValue(),
+ enableSorting: true,
+ }),
+ helper.display({
+ header: 'Connectors',
+ id: 'connectors',
+ cell: (props) => ,
+ size: 100,
+ }),
+ helper.display({
+ header: 'Running tasks',
+ id: 'tasks',
+ cell: (props) => ,
+ size: 100,
+ }),
+];
+
+interface Props {
+ connects: Connect[];
+}
+const List = ({ connects }: Props) => {
+ const navigate = useNavigate();
+ const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
+
+ return (
+ {
+ navigate(`${clusterConnectorsPath(clusterName)}?connect=${name}`);
+ }}
+ emptyMessage="No kafka connect clusters"
+ enableSorting
+ />
+ );
+};
+
+export default List;
diff --git a/frontend/src/components/Connect/Clusters/ui/Statistics/Statistics.tsx b/frontend/src/components/Connect/Clusters/ui/Statistics/Statistics.tsx
new file mode 100644
index 000000000..699840f67
--- /dev/null
+++ b/frontend/src/components/Connect/Clusters/ui/Statistics/Statistics.tsx
@@ -0,0 +1,35 @@
+import React, { useMemo } from 'react';
+import { Connect } from 'generated-sources';
+import * as Statistics from 'components/common/Statistics';
+
+import { computeStatistic } from './models/computeStatistics';
+
+type Props = { connects: Connect[]; isLoading: boolean };
+const ClustersStatistics = ({ connects, isLoading }: Props) => {
+ const statistic = useMemo(() => {
+ return computeStatistic(connects);
+ }, [connects]);
+ return (
+
+
+
+
+
+ );
+};
+
+export default ClustersStatistics;
diff --git a/frontend/src/components/Connect/Clusters/ui/Statistics/models/computeStatistics.ts b/frontend/src/components/Connect/Clusters/ui/Statistics/models/computeStatistics.ts
new file mode 100644
index 000000000..b1452f2fc
--- /dev/null
+++ b/frontend/src/components/Connect/Clusters/ui/Statistics/models/computeStatistics.ts
@@ -0,0 +1,31 @@
+import { Connect } from 'generated-sources';
+
+interface Statistic {
+ clustersCount: number;
+ connectorsCount: number;
+ failedConnectorsCount: number;
+ tasksCount: number;
+ failedTasksCount: number;
+}
+export const computeStatistic = (connects: Connect[]): Statistic => {
+ const clustersCount = connects.length;
+ let connectorsCount = 0;
+ let failedConnectorsCount = 0;
+ let tasksCount = 0;
+ let failedTasksCount = 0;
+
+ connects.forEach((connect) => {
+ connectorsCount += connect.connectorsCount ?? 0;
+ failedConnectorsCount += connect.failedConnectorsCount ?? 0;
+ tasksCount += connect.tasksCount ?? 0;
+ failedTasksCount += connect.failedTasksCount ?? 0;
+ });
+
+ return {
+ clustersCount,
+ connectorsCount,
+ failedConnectorsCount,
+ tasksCount,
+ failedTasksCount,
+ };
+};
diff --git a/frontend/src/components/Connect/Connect.tsx b/frontend/src/components/Connect/Connect.tsx
index 095055cb6..48787f5ae 100644
--- a/frontend/src/components/Connect/Connect.tsx
+++ b/frontend/src/components/Connect/Connect.tsx
@@ -1,44 +1,52 @@
import React from 'react';
-import { Navigate, Routes, Route } from 'react-router-dom';
+import { Navigate, Routes, Route, NavLink } from 'react-router-dom';
import {
- RouteParams,
- clusterConnectConnectorRelativePath,
- clusterConnectConnectorsRelativePath,
- clusterConnectorNewRelativePath,
- getNonExactPath,
clusterConnectorsPath,
+ ClusterNameRoute,
+ kafkaConnectClustersPath,
+ kafkaConnectClustersRelativePath,
+ clusterConnectorsRelativePath,
} from 'lib/paths';
import useAppParams from 'lib/hooks/useAppParams';
-import SuspenseQueryComponent from 'components/common/SuspenseQueryComponent/SuspenseQueryComponent';
+import Navbar from 'components/common/Navigation/Navbar.styled';
+import Clusters from 'components/Connect/Clusters/Clusters';
-import ListPage from './List/ListPage';
-import New from './New/New';
-import DetailsPage from './Details/DetailsPage';
+import Connectors from './List/ListPage';
+import Header from './Header/Header';
const Connect: React.FC = () => {
- const { clusterName } = useAppParams();
+ const { clusterName } = useAppParams();
return (
-
- } />
- } />
-
-
-
- }
- />
- }
- />
- }
- />
-
+ <>
+
+
+ (isActive ? 'is-active' : '')}
+ end
+ >
+ Clusters
+
+ (isActive ? 'is-active' : '')}
+ end
+ >
+ Connectors
+
+
+
+
+ }
+ />
+ } />
+ } />
+
+ >
);
};
diff --git a/frontend/src/components/Connect/Header/Header.tsx b/frontend/src/components/Connect/Header/Header.tsx
new file mode 100644
index 000000000..75b19a6a8
--- /dev/null
+++ b/frontend/src/components/Connect/Header/Header.tsx
@@ -0,0 +1,42 @@
+import { ActionButton } from 'components/common/ActionComponent';
+import ResourcePageHeading from 'components/common/ResourcePageHeading/ResourcePageHeading';
+import Tooltip from 'components/common/Tooltip/Tooltip';
+import ClusterContext from 'components/contexts/ClusterContext';
+import { ResourceType, Action } from 'generated-sources';
+import { useConnects } from 'lib/hooks/api/kafkaConnect';
+import useAppParams from 'lib/hooks/useAppParams';
+import { clusterConnectorNewPath, ClusterNameRoute } from 'lib/paths';
+import React from 'react';
+
+const Header = () => {
+ const { isReadOnly } = React.useContext(ClusterContext);
+ const { clusterName } = useAppParams();
+ const { data: connects = [] } = useConnects(clusterName, true);
+ return (
+
+ {!isReadOnly && (
+
+ Create Connector
+
+ }
+ showTooltip={!connects.length}
+ content="No Connects available"
+ placement="left"
+ />
+ )}
+
+ );
+};
+
+export default Header;
diff --git a/frontend/src/components/Connect/Header/__tests__/Header.spec.tsx b/frontend/src/components/Connect/Header/__tests__/Header.spec.tsx
new file mode 100644
index 000000000..8f1bd93d1
--- /dev/null
+++ b/frontend/src/components/Connect/Header/__tests__/Header.spec.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import Header from 'components/Connect/Header/Header';
+import ClusterContext, {
+ initialValue,
+} from 'components/contexts/ClusterContext';
+import { render } from 'lib/testHelpers';
+import { screen } from '@testing-library/react';
+import { connects } from 'lib/fixtures/kafkaConnect';
+import { useConnects } from 'lib/hooks/api/kafkaConnect';
+
+jest.mock('lib/hooks/api/kafkaConnect', () => ({
+ useConnects: jest.fn(),
+}));
+
+describe('Kafka Connect header', () => {
+ beforeEach(() => {
+ (useConnects as jest.Mock).mockImplementation(() => ({
+ data: connects,
+ }));
+ });
+
+ async function renderComponent({ isReadOnly }: { isReadOnly: boolean }) {
+ render(
+
+
+
+ );
+ }
+
+ it('render create connector button', () => {
+ renderComponent({ isReadOnly: false });
+
+ const btn = screen.getByRole('button', { name: 'Create Connector' });
+
+ expect(btn).toBeInTheDocument();
+ expect(btn).toBeEnabled();
+ });
+
+ describe('when no connects', () => {
+ it('create connector button is disabled', () => {
+ renderComponent({ isReadOnly: false });
+
+ const btn = screen.getByRole('button', { name: 'Create Connector' });
+
+ expect(btn).toBeInTheDocument();
+ expect(btn).toBeEnabled();
+ });
+ });
+
+ describe('when cluster is readonly', () => {
+ it('doesnt render create connector button', () => {
+ (useConnects as jest.Mock).mockImplementation(() => ({
+ data: [],
+ }));
+ renderComponent({ isReadOnly: false });
+
+ const btn = screen.queryByRole('button', { name: 'Create Connector' });
+
+ expect(btn).toBeDisabled();
+ });
+ });
+});
diff --git a/frontend/src/components/Connect/List/ListPage.styled.ts b/frontend/src/components/Connect/List/ListPage.styled.ts
new file mode 100644
index 000000000..636351d47
--- /dev/null
+++ b/frontend/src/components/Connect/List/ListPage.styled.ts
@@ -0,0 +1,6 @@
+import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
+import styled from 'styled-components';
+
+export const Search = styled(ControlPanelWrapper)`
+ padding-top: 24px;
+`;
diff --git a/frontend/src/components/Connect/List/ListPage.tsx b/frontend/src/components/Connect/List/ListPage.tsx
index 3397d339f..3b257dd6d 100644
--- a/frontend/src/components/Connect/List/ListPage.tsx
+++ b/frontend/src/components/Connect/List/ListPage.tsx
@@ -1,89 +1,24 @@
import React, { Suspense } from 'react';
import useAppParams from 'lib/hooks/useAppParams';
-import { ClusterNameRoute, clusterConnectorNewRelativePath } from 'lib/paths';
-import ClusterContext from 'components/contexts/ClusterContext';
+import { ClusterNameRoute } from 'lib/paths';
import Search from 'components/common/Search/Search';
-import * as Metrics from 'components/common/Metrics';
-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 ResourcePageHeading from 'components/common/ResourcePageHeading/ResourcePageHeading';
+import { useConnectors } from 'lib/hooks/api/kafkaConnect';
+import * as S from './ListPage.styled';
import List from './List';
+import ConnectorsStatistics from './Statistics/Statistics';
const ListPage: React.FC = () => {
- const { isReadOnly } = React.useContext(ClusterContext);
const { clusterName } = useAppParams();
- 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 numberOfFailedConnectors = connectorsMetrics?.filter(
- ({ status: { state } }) => state === ConnectorState.FAILED
- ).length;
-
- const numberOfFailedTasks = connectorsMetrics?.reduce(
- (acc, metric) => acc + (metric.failedTasksCount ?? 0),
- 0
- );
+ const { data, isLoading } = useConnectors(clusterName);
return (
<>
-
- {!isReadOnly && (
-
- Create Connector
-
- }
- showTooltip={!connects.length}
- content="No Connects available"
- placement="left"
- />
- )}
-
-
-
-
- {connectorsMetrics?.length || '-'}
-
-
- {numberOfFailedConnectors ?? '-'}
-
-
- {numberOfFailedTasks ?? '-'}
-
-
-
-
+
+
-
+
}>
diff --git a/frontend/src/components/Connect/List/RunningTasksCell.tsx b/frontend/src/components/Connect/List/RunningTasksCell.tsx
index 4c3293d44..fc829df49 100644
--- a/frontend/src/components/Connect/List/RunningTasksCell.tsx
+++ b/frontend/src/components/Connect/List/RunningTasksCell.tsx
@@ -1,21 +1,32 @@
import React from 'react';
import { FullConnectorInfo } from 'generated-sources';
import { CellContext } from '@tanstack/react-table';
+import AlertBadge from 'components/common/AlertBadge/AlertBadge';
const RunningTasksCell: React.FC> = ({
row,
}) => {
const { tasksCount, failedTasksCount } = row.original;
+ const failedCount = failedTasksCount ?? 0;
+ const count = tasksCount ?? 0;
+
+ const text = `${count - failedCount}/${count}`;
+
if (!tasksCount) {
return null;
}
- return (
- <>
- {tasksCount - (failedTasksCount || 0)} of {tasksCount}
- >
- );
+ if (failedCount > 0) {
+ return (
+
+
+
+
+ );
+ }
+
+ return text;
};
export default RunningTasksCell;
diff --git a/frontend/src/components/Connect/List/Statistics/Statistics.tsx b/frontend/src/components/Connect/List/Statistics/Statistics.tsx
new file mode 100644
index 000000000..4b9dd6da7
--- /dev/null
+++ b/frontend/src/components/Connect/List/Statistics/Statistics.tsx
@@ -0,0 +1,34 @@
+import React, { useMemo } from 'react';
+import * as Statistics from 'components/common/Statistics';
+import { FullConnectorInfo } from 'generated-sources';
+
+import { computeStatistics } from './models/computeStatistics';
+
+type Props = {
+ connectors: FullConnectorInfo[];
+ isLoading: boolean;
+};
+const ConnectorsStatistics = ({ connectors, isLoading }: Props) => {
+ const statistics = useMemo(() => {
+ return computeStatistics(connectors);
+ }, [connectors]);
+
+ return (
+
+
+
+
+ );
+};
+
+export default ConnectorsStatistics;
diff --git a/frontend/src/components/Connect/List/Statistics/__tests__/Statistics.spec.tsx b/frontend/src/components/Connect/List/Statistics/__tests__/Statistics.spec.tsx
new file mode 100644
index 000000000..a1f1f382d
--- /dev/null
+++ b/frontend/src/components/Connect/List/Statistics/__tests__/Statistics.spec.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { render } from 'lib/testHelpers';
+import { screen, within } from '@testing-library/react';
+import Statistics from 'components/Connect/List/Statistics/Statistics';
+import { FullConnectorInfo } from 'generated-sources';
+import { connectors } from 'lib/fixtures/kafkaConnect';
+
+describe('Kafka Connect Connectors Statistics', () => {
+ async function renderComponent({
+ data,
+ isLoading,
+ }: {
+ data: FullConnectorInfo[] | undefined;
+ isLoading: boolean;
+ }) {
+ render();
+ }
+
+ describe('when data loading', () => {
+ let statistics: HTMLElement;
+ beforeEach(() => {
+ renderComponent({ data: undefined, isLoading: true });
+ statistics = screen.getByRole('group');
+ });
+
+ it('renders statistic container elements', () => {
+ expect(statistics).toBeInTheDocument();
+ });
+
+ describe('Connectors statistic', () => {
+ let connectorStatisticsCell: HTMLElement;
+ beforeEach(() => {
+ connectorStatisticsCell = within(statistics).getByRole('cell', {
+ name: 'Connectors',
+ });
+ });
+ it('exists', () => {
+ expect(connectorStatisticsCell).toBeInTheDocument();
+ });
+
+ it('shows loader', () => {
+ const loader = within(connectorStatisticsCell).getByRole('status');
+ expect(loader).toBeInTheDocument();
+ });
+ });
+ describe('Tasks statistic', () => {
+ let tasksStatisticsCell: HTMLElement;
+ beforeEach(() => {
+ tasksStatisticsCell = within(statistics).getByRole('cell', {
+ name: 'Tasks',
+ });
+ });
+ it('exists', () => {
+ expect(tasksStatisticsCell).toBeInTheDocument();
+ });
+
+ it('shows loader', () => {
+ const loader = within(tasksStatisticsCell).getByRole('status');
+ expect(loader).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('when data is loaded', () => {
+ let connectorsStatistic: HTMLElement;
+ let tasksStatistics: HTMLElement;
+ beforeEach(() => {
+ renderComponent({ data: connectors, isLoading: false });
+ [connectorsStatistic, tasksStatistics] = screen.getAllByRole('cell');
+ });
+
+ describe('Connectors statistics', () => {
+ it('renders statistic', () => {
+ expect(connectorsStatistic).toBeInTheDocument();
+
+ const count = within(connectorsStatistic).getByText(3);
+ expect(count).toBeInTheDocument();
+
+ const alert = within(connectorsStatistic).getByRole('alert');
+ expect(alert).toBeInTheDocument();
+ expect(alert).toHaveTextContent('1');
+ });
+ });
+ describe('Tasks statistics', () => {
+ it('renders statistic', () => {
+ expect(tasksStatistics).toBeInTheDocument();
+
+ const count = within(tasksStatistics).getByText(5);
+ expect(count).toBeInTheDocument();
+
+ const alert = within(tasksStatistics).getByRole('alert');
+ expect(alert).toBeInTheDocument();
+ expect(alert).toHaveTextContent('1');
+ });
+ });
+ });
+});
diff --git a/frontend/src/components/Connect/List/Statistics/models/computeStatistics.ts b/frontend/src/components/Connect/List/Statistics/models/computeStatistics.ts
new file mode 100644
index 000000000..ad3b85a1d
--- /dev/null
+++ b/frontend/src/components/Connect/List/Statistics/models/computeStatistics.ts
@@ -0,0 +1,33 @@
+import { ConnectorState, FullConnectorInfo } from 'generated-sources';
+
+interface Statistic {
+ connectorsCount: number;
+ failedConnectorsCount: number;
+ tasksCount: number;
+ failedTasksCount: number;
+}
+
+export const computeStatistics = (
+ connectors: FullConnectorInfo[]
+): Statistic => {
+ const connectorsCount = connectors.length;
+ let failedConnectorsCount = 0;
+ let tasksCount = 0;
+ let failedTasksCount = 0;
+
+ connectors.forEach((connector) => {
+ if (connector.status.state === ConnectorState.FAILED) {
+ failedConnectorsCount += 1;
+ }
+
+ tasksCount += connector.tasksCount ?? 0;
+ failedTasksCount += connector.failedTasksCount ?? 0;
+ });
+
+ return {
+ connectorsCount,
+ failedConnectorsCount,
+ tasksCount,
+ failedTasksCount,
+ };
+};
diff --git a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx
index 0cbe4dd78..77196a1be 100644
--- a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx
+++ b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx
@@ -1,11 +1,11 @@
import React from 'react';
-import { connectors, connects } from 'lib/fixtures/kafkaConnect';
+import { connects } from 'lib/fixtures/kafkaConnect';
import ClusterContext, {
ContextProps,
initialValue,
} from 'components/contexts/ClusterContext';
import ListPage from 'components/Connect/List/ListPage';
-import { screen, within } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import { render, WithRoute } from 'lib/testHelpers';
import { clusterConnectorsPath } from 'lib/paths';
import { useConnectors, useConnects } from 'lib/hooks/api/kafkaConnect';
@@ -45,28 +45,6 @@ describe('Connectors List Page', () => {
{ initialEntries: [clusterConnectorsPath(clusterName)] }
);
- describe('Heading', () => {
- it('renders header without create button for readonly cluster', async () => {
- await renderComponent({ ...initialValue, isReadOnly: true });
- expect(
- screen.getByRole('heading', { name: 'Connectors' })
- ).toBeInTheDocument();
- expect(
- screen.queryByRole('link', { name: 'Create Connector' })
- ).not.toBeInTheDocument();
- });
-
- it('renders header with create button for read/write cluster', async () => {
- await renderComponent();
- expect(
- screen.getByRole('heading', { name: 'Connectors' })
- ).toBeInTheDocument();
- expect(
- screen.getByRole('link', { name: 'Create Connector' })
- ).toBeInTheDocument();
- });
- });
-
it('renders search input', async () => {
await renderComponent();
expect(
@@ -78,127 +56,4 @@ describe('Connectors List Page', () => {
await renderComponent();
expect(screen.getByText('Connectors List')).toBeInTheDocument();
});
-
- describe('Metrics', () => {
- it('renders indicators in loading state', async () => {
- (useConnectors as jest.Mock).mockImplementation(() => ({
- isLoading: true,
- data: connectors,
- }));
-
- await renderComponent();
- const metrics = screen.getByRole('group');
- expect(metrics).toBeInTheDocument();
- expect(within(metrics).getAllByText('progressbar').length).toEqual(3);
- });
-
- it('renders indicators for empty list of connectors', async () => {
- await renderComponent();
- const metrics = screen.getByRole('group');
- expect(metrics).toBeInTheDocument();
-
- const connectorsIndicator = within(metrics).getByTitle(
- 'Total number of connectors'
- );
- expect(connectorsIndicator).toBeInTheDocument();
- expect(connectorsIndicator).toHaveTextContent('Connectors -');
-
- const failedConnectorsIndicator = within(metrics).getByTitle(
- 'Number of failed connectors'
- );
- expect(failedConnectorsIndicator).toBeInTheDocument();
- expect(failedConnectorsIndicator).toHaveTextContent(
- 'Failed Connectors 0'
- );
-
- const failedTasksIndicator = within(metrics).getByTitle(
- 'Number of failed tasks'
- );
- expect(failedTasksIndicator).toBeInTheDocument();
- expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 0');
- });
-
- it('renders indicators when connectors list is undefined', async () => {
- (useConnectors as jest.Mock).mockImplementation(() => ({
- isFetching: false,
- data: undefined,
- }));
-
- await renderComponent();
- const metrics = screen.getByRole('group');
- expect(metrics).toBeInTheDocument();
-
- const connectorsIndicator = within(metrics).getByTitle(
- 'Total number of connectors'
- );
- expect(connectorsIndicator).toBeInTheDocument();
- expect(connectorsIndicator).toHaveTextContent('Connectors -');
-
- const failedConnectorsIndicator = within(metrics).getByTitle(
- 'Number of failed connectors'
- );
- expect(failedConnectorsIndicator).toBeInTheDocument();
- expect(failedConnectorsIndicator).toHaveTextContent(
- 'Failed Connectors -'
- );
-
- const failedTasksIndicator = within(metrics).getByTitle(
- 'Number of failed tasks'
- );
- expect(failedTasksIndicator).toBeInTheDocument();
- expect(failedTasksIndicator).toHaveTextContent('Failed Tasks -');
- });
-
- it('renders indicators list of connectors', async () => {
- (useConnectors as jest.Mock).mockImplementation(() => ({
- isLoading: false,
- data: connectors,
- }));
-
- await renderComponent();
-
- const metrics = screen.getByRole('group');
- expect(metrics).toBeInTheDocument();
-
- const connectorsIndicator = within(metrics).getByTitle(
- 'Total number of connectors'
- );
- expect(connectorsIndicator).toBeInTheDocument();
- expect(connectorsIndicator).toHaveTextContent(
- `Connectors ${connectors.length}`
- );
-
- const failedConnectorsIndicator = within(metrics).getByTitle(
- 'Number of failed connectors'
- );
- expect(failedConnectorsIndicator).toBeInTheDocument();
- expect(failedConnectorsIndicator).toHaveTextContent(
- 'Failed Connectors 1'
- );
-
- const failedTasksIndicator = within(metrics).getByTitle(
- 'Number of failed tasks'
- );
- expect(failedTasksIndicator).toBeInTheDocument();
- expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 1');
- });
- });
-
- describe('Create new connector', () => {
- it('Create new connector button is enabled when connects list is not empty', async () => {
- await renderComponent();
-
- expect(screen.getByText('Create Connector')).toBeEnabled();
- });
-
- it('Create new connector button is disabled when connects list is empty', async () => {
- (useConnects as jest.Mock).mockImplementation(() => ({
- data: [],
- }));
-
- await renderComponent();
-
- expect(screen.getByText('Create Connector')).toBeDisabled();
- });
- });
});
diff --git a/frontend/src/components/Connect/__tests__/Connect.spec.tsx b/frontend/src/components/Connect/__tests__/Connect.spec.tsx
index 855e54645..4f96d7931 100644
--- a/frontend/src/components/Connect/__tests__/Connect.spec.tsx
+++ b/frontend/src/components/Connect/__tests__/Connect.spec.tsx
@@ -2,13 +2,7 @@ import React from 'react';
import { render, WithRoute } from 'lib/testHelpers';
import { screen } from '@testing-library/react';
import Connect from 'components/Connect/Connect';
-import {
- clusterConnectorsPath,
- clusterConnectorNewPath,
- clusterConnectConnectorPath,
- getNonExactPath,
- clusterConnectsPath,
-} from 'lib/paths';
+import { getNonExactPath, kafkaConnectPath } from 'lib/paths';
const ConnectCompText = {
new: 'New Page',
@@ -35,27 +29,22 @@ describe('Connect', () => {
{ initialEntries: [pathname] }
);
- it('renders ListPage', () => {
- renderComponent(
- clusterConnectorsPath('my-cluster'),
- clusterConnectorsPath()
- );
- expect(screen.getByText(ConnectCompText.list)).toBeInTheDocument();
- });
+ it('renders header', () => {
+ renderComponent(kafkaConnectPath('my-cluster'), kafkaConnectPath());
- it('renders New Page', () => {
- renderComponent(
- clusterConnectorNewPath('my-cluster'),
- clusterConnectorsPath()
- );
- expect(screen.getByText(ConnectCompText.new)).toBeInTheDocument();
+ const header = screen.getByRole('heading', { name: 'Kafka Connect' });
+ expect(header).toBeInTheDocument();
});
- it('renders Details Page', () => {
- renderComponent(
- clusterConnectConnectorPath('my-cluster', 'my-connect', 'my-connector'),
- clusterConnectsPath()
- );
- expect(screen.getByText(ConnectCompText.details)).toBeInTheDocument();
+ it('renders navigation', () => {
+ renderComponent(kafkaConnectPath('my-cluster'), kafkaConnectPath());
+
+ const clusterNavigation = screen.getByRole('link', { name: 'Clusters' });
+ expect(clusterNavigation).toBeInTheDocument();
+
+ const connectorsNavigation = screen.getByRole('link', {
+ name: 'Connectors',
+ });
+ expect(connectorsNavigation).toBeInTheDocument();
});
});
diff --git a/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx b/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx
index 8f73a96ec..6eeca0e48 100644
--- a/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx
+++ b/frontend/src/components/Nav/ClusterMenu/ClusterMenu.tsx
@@ -6,12 +6,13 @@ import MenuItem from 'components/Nav/Menu/MenuItem';
import {
clusterACLPath,
clusterBrokersPath,
- clusterConnectorsPath,
clusterConnectsPath,
+ clusterConnectorsPath,
clusterConsumerGroupsPath,
clusterKsqlDbPath,
clusterSchemasPath,
clusterTopicsPath,
+ kafkaConnectPath,
} from 'lib/paths';
import { useLocation } from 'react-router-dom';
import { useLocalStorage } from 'lib/hooks/useLocalStorage';
@@ -83,10 +84,11 @@ const ClusterMenu: FC = ({
{hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_CONNECT) && (
)}
diff --git a/frontend/src/components/common/AlertBadge/AlertBadge.styled.tsx b/frontend/src/components/common/AlertBadge/AlertBadge.styled.tsx
new file mode 100644
index 000000000..495d3b2b2
--- /dev/null
+++ b/frontend/src/components/common/AlertBadge/AlertBadge.styled.tsx
@@ -0,0 +1,18 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ height: 24px;
+ border-radius: 24px;
+ background-color: ${({ theme }) => theme.alertBadge.background};
+ display: inline-flex;
+ padding: 2px 8px;
+ gap: 4px;
+ align-items: center;
+`;
+
+export const Content = styled.div`
+ font-weight: 400;
+ size: 14px;
+ line-height: 20px;
+ color: ${({ theme }) => theme.alertBadge.content.color};
+`;
diff --git a/frontend/src/components/common/AlertBadge/AlertBadge.tsx b/frontend/src/components/common/AlertBadge/AlertBadge.tsx
new file mode 100644
index 000000000..4a5839b9e
--- /dev/null
+++ b/frontend/src/components/common/AlertBadge/AlertBadge.tsx
@@ -0,0 +1,26 @@
+import React, { PropsWithChildren } from 'react';
+import { useTheme } from 'styled-components';
+import AlertIcon from 'components/common/Icons/AlertIcon';
+
+import * as S from './AlertBadge.styled';
+
+interface AlertBadgeProps {}
+function AlertBadge({ children }: PropsWithChildren) {
+ return {children};
+}
+
+const Icon = () => {
+ const theme = useTheme();
+ return ;
+};
+
+interface AlertBadgeContentProps {
+ content: string | number;
+}
+const Content = ({ content }: AlertBadgeContentProps) => {
+ return {content};
+};
+
+AlertBadge.Icon = Icon;
+AlertBadge.Content = Content;
+export default AlertBadge;
diff --git a/frontend/src/components/common/Icons/AlertIcon.tsx b/frontend/src/components/common/Icons/AlertIcon.tsx
index 3c79f78e6..c8312947f 100644
--- a/frontend/src/components/common/Icons/AlertIcon.tsx
+++ b/frontend/src/components/common/Icons/AlertIcon.tsx
@@ -1,6 +1,10 @@
import React from 'react';
-const AlertIcon: React.FC = () => {
+interface Props {
+ fill?: string;
+}
+
+const AlertIcon: React.FC = ({ fill }) => {
return (
);
diff --git a/frontend/src/components/common/Statistics/Statistic/Statistic.styled.tsx b/frontend/src/components/common/Statistics/Statistic/Statistic.styled.tsx
new file mode 100644
index 000000000..6e61c39bc
--- /dev/null
+++ b/frontend/src/components/common/Statistics/Statistic/Statistic.styled.tsx
@@ -0,0 +1,27 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ width: 216px;
+ height: 68px;
+ border-radius: 12px;
+ padding: 12px 16px;
+ background: ${({ theme }) => theme.kafkaConectClusters.statistic.background};
+`;
+
+export const Header = styled.span`
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 16px;
+ color: ${({ theme }) => theme.kafkaConectClusters.statistic.header.color};
+`;
+export const Footer = styled.div`
+ display: flex;
+ justify-content: space-between;
+`;
+
+export const Count = styled.div`
+ font-weight: 500;
+ font-size: 16px;
+ line-height: 24px;
+ color: ${({ theme }) => theme.kafkaConectClusters.statistic.count.color};
+`;
diff --git a/frontend/src/components/common/Statistics/Statistic/Statistic.tsx b/frontend/src/components/common/Statistics/Statistic/Statistic.tsx
new file mode 100644
index 000000000..f71c03c60
--- /dev/null
+++ b/frontend/src/components/common/Statistics/Statistic/Statistic.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import AlertBadge from 'components/common/AlertBadge/AlertBadge';
+import SpinnerIcon from 'components/common/Icons/SpinnerIcon';
+
+import * as S from './Statistic.styled';
+
+type Props = {
+ title: string;
+ count: number;
+ isLoading: boolean;
+ warningCount?: number;
+};
+const Statistic = ({ title, count, warningCount, isLoading }: Props) => {
+ const showWarning = warningCount !== undefined && warningCount > 0;
+ return (
+
+ {title}
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {count}
+ {showWarning && (
+
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+};
+
+export default Statistic;
diff --git a/frontend/src/components/common/Statistics/Statistics.styled.tsx b/frontend/src/components/common/Statistics/Statistics.styled.tsx
new file mode 100644
index 000000000..06299e8cb
--- /dev/null
+++ b/frontend/src/components/common/Statistics/Statistics.styled.tsx
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ padding: 16px;
+ display: flex;
+ gap: 4px;
+ background: ${({ theme }) => theme.kafkaConectClusters.statistics.background};
+`;
diff --git a/frontend/src/components/common/Statistics/index.ts b/frontend/src/components/common/Statistics/index.ts
new file mode 100644
index 000000000..ceb22b87e
--- /dev/null
+++ b/frontend/src/components/common/Statistics/index.ts
@@ -0,0 +1,4 @@
+import Item from './Statistic/Statistic';
+
+export { Container } from './Statistics.styled';
+export { Item };
diff --git a/frontend/src/lib/__test__/paths.spec.ts b/frontend/src/lib/__test__/paths.spec.ts
index c6abb7707..4a731e393 100644
--- a/frontend/src/lib/__test__/paths.spec.ts
+++ b/frontend/src/lib/__test__/paths.spec.ts
@@ -240,17 +240,22 @@ describe('Paths', () => {
paths.clusterConnectsPath(RouteParams.clusterName)
);
});
- it('clusterConnectorsPath', () => {
+ it('kafkaConnectConnectorsPath', () => {
expect(paths.clusterConnectorsPath(clusterName)).toEqual(
- `${paths.clusterPath(clusterName)}/connectors`
+ `${paths.clusterPath(clusterName)}/kafka-connect/connectors`
);
expect(paths.clusterConnectorsPath()).toEqual(
paths.clusterConnectorsPath(RouteParams.clusterName)
);
});
+ it('kafkaConnectConnectorsPath', () => {
+ expect(paths.kafkaConnectClustersPath(clusterName)).toEqual(
+ `${paths.clusterPath(clusterName)}/kafka-connect/clusters`
+ );
+ });
it('clusterConnectorNewPath', () => {
expect(paths.clusterConnectorNewPath(clusterName)).toEqual(
- `${paths.clusterConnectorsPath(clusterName)}/create-new`
+ `${paths.clusterPath(clusterName)}/create-new`
);
expect(paths.clusterConnectorNewPath()).toEqual(
paths.clusterConnectorNewPath(RouteParams.clusterName)
diff --git a/frontend/src/lib/hooks/api/kafkaConnect.ts b/frontend/src/lib/hooks/api/kafkaConnect.ts
index 743d78307..64ce2af41 100644
--- a/frontend/src/lib/hooks/api/kafkaConnect.ts
+++ b/frontend/src/lib/hooks/api/kafkaConnect.ts
@@ -19,10 +19,11 @@ interface CreateConnectorProps {
newConnector: NewConnector;
}
-const connectsKey = (clusterName: ClusterName) => [
+const connectsKey = (clusterName: ClusterName, withStats?: boolean) => [
'clusters',
clusterName,
'connects',
+ withStats,
];
const connectorsKey = (clusterName: ClusterName, search?: string) => {
const base = ['clusters', clusterName, 'connectors'];
@@ -44,9 +45,9 @@ const connectorTasksKey = (props: UseConnectorProps) => [
'tasks',
];
-export function useConnects(clusterName: ClusterName) {
- return useQuery(connectsKey(clusterName), () =>
- api.getConnects({ clusterName })
+export function useConnects(clusterName: ClusterName, withStats?: boolean) {
+ return useQuery(connectsKey(clusterName, withStats), () =>
+ api.getConnects({ clusterName, withStats })
);
}
export function useConnectors(clusterName: ClusterName, search?: string) {
diff --git a/frontend/src/lib/paths.ts b/frontend/src/lib/paths.ts
index b39ee5e6a..dbef8246b 100644
--- a/frontend/src/lib/paths.ts
+++ b/frontend/src/lib/paths.ts
@@ -207,18 +207,31 @@ export type RouteParamsClusterTopic = {
// Kafka Connect
export const clusterConnectsRelativePath = 'connects';
export const clusterConnectorsRelativePath = 'connectors';
-export const clusterConnectorNewRelativePath = 'create-new';
+export const kafkaConnectRelativePath = 'kafka-connect';
+export const kafkaConnectClustersRelativePath = 'clusters';
+
export const clusterConnectConnectorsRelativePath = `${RouteParams.connectName}/connectors`;
-export const clusterConnectConnectorRelativePath = `${clusterConnectConnectorsRelativePath}/${RouteParams.connectorName}`;
+export const clusterConnectConnectorRelativePath = `${clusterConnectsRelativePath}/${clusterConnectConnectorsRelativePath}/${RouteParams.connectorName}`;
+export const clusterConnectorNewRelativePath = 'create-new';
const clusterConnectConnectorTasksRelativePath = 'tasks';
export const clusterConnectConnectorConfigRelativePath = 'config';
export const clusterConnectsPath = (
clusterName: ClusterName = RouteParams.clusterName
) => `${clusterPath(clusterName)}/connects`;
+
+export const kafkaConnectPath = (
+ clusterName: ClusterName = RouteParams.clusterName
+) => `${clusterPath(clusterName)}/kafka-connect`;
+export const kafkaConnectClustersPath = (
+ clusterName: ClusterName = RouteParams.clusterName
+) => {
+ return `${kafkaConnectPath(clusterName)}/clusters`;
+};
+
export const clusterConnectorsPath = (
clusterName: ClusterName = RouteParams.clusterName
-) => `${clusterPath(clusterName)}/connectors`;
+) => `${kafkaConnectPath(clusterName)}/connectors`;
export const clusterConnectorNameSubPath = (
clusterName: ClusterName = RouteParams.connectorName
) => {
@@ -227,7 +240,7 @@ export const clusterConnectorNameSubPath = (
export const clusterConnectorNewPath = (
clusterName: ClusterName = RouteParams.clusterName
-) => `${clusterConnectorsPath(clusterName)}/create-new`;
+) => `${clusterPath(clusterName)}/create-new`;
export const clusterConnectConnectorsPath = (
clusterName: ClusterName = RouteParams.clusterName,
connectName: Connect['name'] = RouteParams.connectName
diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts
index c2ccdb449..cfe47f721 100644
--- a/frontend/src/theme/theme.ts
+++ b/frontend/src/theme/theme.ts
@@ -61,6 +61,10 @@ const Colors = {
'55': '#CF1717',
'60': '#B81414',
},
+ orange: {
+ '10': '#BF83401A',
+ '100': '#FF9D00',
+ },
yellow: {
'10': '#FFEECC',
'20': '#FFDD57',
@@ -404,6 +408,29 @@ const baseTheme = {
export const theme = {
...baseTheme,
+ alertBadge: {
+ background: Colors.orange[10],
+ content: {
+ color: Colors.neutral[90],
+ },
+ icon: {
+ color: Colors.orange[100],
+ },
+ },
+ kafkaConectClusters: {
+ statistics: {
+ background: Colors.neutral[5],
+ },
+ statistic: {
+ background: Colors.neutral[0],
+ count: {
+ color: Colors.neutral[90],
+ },
+ header: {
+ color: Colors.neutral[50],
+ },
+ },
+ },
logo: {
color: Colors.brand[90],
},
@@ -929,6 +956,29 @@ export type ClusterColorKey =
export const darkTheme: ThemeType = {
...baseTheme,
+ alertBadge: {
+ background: Colors.orange[10],
+ content: {
+ color: Colors.neutral[0],
+ },
+ icon: {
+ color: Colors.orange[100],
+ },
+ },
+ kafkaConectClusters: {
+ statistics: {
+ background: Colors.neutral[95],
+ },
+ statistic: {
+ background: Colors.neutral[90],
+ count: {
+ color: Colors.neutral[0],
+ },
+ header: {
+ color: Colors.neutral[50],
+ },
+ },
+ },
auth_page: {
backgroundColor: Colors.neutral[90],
fontFamily: baseTheme.auth_page.fontFamily,