Skip to content

Commit 13f545b

Browse files
mehmetcangulesciMehmetcan Güleşçi
andauthored
KC: Show connector-level trace when status is FAILED (#1319)
Co-authored-by: Mehmetcan Güleşçi <[email protected]>
1 parent d6cbc7f commit 13f545b

File tree

11 files changed

+332
-51
lines changed

11 files changed

+332
-51
lines changed

api/src/test/java/io/kafbat/ui/mapper/KafkaConnectMapperTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ void toKafkaConnect() {
4242

4343
ConnectorDTO connectorDto = new ConnectorDTO();
4444
connectorDto.setName(UUID.randomUUID().toString());
45+
46+
String traceMessage = connectorState == ConnectorStateDTO.FAILED
47+
? "Test error trace for failed connector"
48+
: null;
49+
4550
connectorDto.setStatus(
46-
new ConnectorStatusDTO(connectorState, UUID.randomUUID().toString())
51+
new ConnectorStatusDTO(connectorState, UUID.randomUUID().toString(), traceMessage)
4752
);
4853

4954
List<TaskDTO> tasks = new ArrayList<>();

api/src/test/java/io/kafbat/ui/service/index/KafkaConnectNgramFilterTest.java

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,22 @@ class KafkaConnectNgramFilterTest extends AbstractNgramFilterTest<FullConnectorI
1414

1515
@Override
1616
protected NgramFilter<FullConnectorInfoDTO> buildFilter(List<FullConnectorInfoDTO> items,
17-
boolean enabled,
18-
ClustersProperties.NgramProperties ngramProperties) {
17+
boolean enabled,
18+
ClustersProperties.NgramProperties ngramProperties) {
1919
return new KafkaConnectNgramFilter(items, enabled, ngramProperties);
2020
}
2121

2222
@Override
2323
protected List<FullConnectorInfoDTO> items() {
24-
return IntStream.range(0, 100).mapToObj(i ->
25-
new FullConnectorInfoDTO(
26-
"connect-" + i,
27-
"connector-" + i,
28-
"class",
29-
ConnectorTypeDTO.SINK,
30-
List.of(),
31-
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, "reason"),
32-
1,
33-
0
34-
)
35-
).toList();
24+
return IntStream.range(0, 100).mapToObj(i -> new FullConnectorInfoDTO(
25+
"connect-" + i,
26+
"connector-" + i,
27+
"class",
28+
ConnectorTypeDTO.SINK,
29+
List.of(),
30+
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, "worker-1", "reason"),
31+
1,
32+
0)).toList();
3633
}
3734

3835
@Override
@@ -55,7 +52,7 @@ protected List<FullConnectorInfoDTO> sortedItems() {
5552
"class",
5653
ConnectorTypeDTO.SINK,
5754
List.of(),
58-
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, "reason"),
55+
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, null, "reason"),
5956
1,
6057
0
6158
),
@@ -65,7 +62,7 @@ protected List<FullConnectorInfoDTO> sortedItems() {
6562
"class",
6663
ConnectorTypeDTO.SINK,
6764
List.of(),
68-
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, "reason"),
65+
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, null, "reason"),
6966
1,
7067
0
7168
)
@@ -86,20 +83,17 @@ protected List<FullConnectorInfoDTO> sortedResult(List<FullConnectorInfoDTO> ite
8683
"class",
8784
ConnectorTypeDTO.SINK,
8885
List.of(),
89-
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, "reason"),
86+
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, null, "reason"),
9087
1,
91-
0
92-
),
88+
0),
9389
new FullConnectorInfoDTO(
9490
"connect-pay",
9591
"connector-pay",
9692
"class",
9793
ConnectorTypeDTO.SINK,
9894
List.of(),
99-
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, "reason"),
95+
new ConnectorStatusDTO(ConnectorStateDTO.RUNNING, null, "reason"),
10096
1,
101-
0
102-
)
103-
);
97+
0));
10498
}
10599
}

contract-typespec/api/kafka-connect.tsp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ enum ConnectorState {
217217
model ConnectorStatus {
218218
state: ConnectorState;
219219
workerId?: string;
220+
trace?: string;
220221
}
221222

222223
model Connector {

contract/src/main/resources/swagger/kafbat-ui-api.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3756,6 +3756,8 @@ components:
37563756
$ref: '#/components/schemas/ConnectorState'
37573757
workerId:
37583758
type: string
3759+
trace:
3760+
type: string
37593761
required:
37603762
- state
37613763

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import styled, { css } from 'styled-components';
2+
3+
export const WorkerInfo = styled.p(
4+
({ theme: { modal } }) => css`
5+
margin: 4px 0 0 0;
6+
font-size: 14px;
7+
color: ${modal.contentColor};
8+
`
9+
);
10+
11+
export const TraceContent = styled.div(
12+
({ theme: { modal } }) => css`
13+
background-color: ${modal.border.contrast};
14+
padding: 16px;
15+
border-radius: 6px;
16+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
17+
font-size: 12px;
18+
color: ${modal.color};
19+
border: 1px solid ${modal.border.contrast};
20+
max-height: 400px;
21+
overflow-y: auto;
22+
white-space: pre-wrap;
23+
word-break: break-word;
24+
`
25+
);

frontend/src/components/Connect/Details/Overview/Overview.tsx

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22
import * as C from 'components/common/Tag/Tag.styled';
33
import * as Metrics from 'components/common/Metrics';
4+
import { Button } from 'components/common/Button/Button';
5+
import { Modal } from 'components/common/Modal';
46
import getTagColor from 'components/common/Tag/getTagColor';
57
import { RouterParamsClusterConnectConnector } from 'lib/paths';
68
import useAppParams from 'lib/hooks/useAppParams';
79
import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
10+
import { ConnectorState, Connector } from 'generated-sources';
811

912
import getTaskMetrics from './getTaskMetrics';
13+
import * as S from './Overview.styled';
1014

1115
const Overview: React.FC = () => {
1216
const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
17+
const [showTraceModal, setShowTraceModal] = useState(false);
1318

1419
const { data: connector } = useConnector(routerProps);
1520
const { data: tasks } = useConnectorTasks(routerProps);
@@ -20,35 +25,78 @@ const Overview: React.FC = () => {
2025

2126
const { running, failed } = getTaskMetrics(tasks);
2227

28+
const canShowTrace = (connectorData: Connector) =>
29+
connectorData.status.state === ConnectorState.FAILED &&
30+
!!connectorData.status.trace;
31+
32+
const handleStateClick = () => {
33+
if (canShowTrace(connector)) {
34+
setShowTraceModal(true);
35+
}
36+
};
37+
2338
return (
24-
<Metrics.Wrapper>
25-
<Metrics.Section>
26-
{connector.status?.workerId && (
27-
<Metrics.Indicator label="Worker">
28-
{connector.status.workerId}
39+
<>
40+
<Metrics.Wrapper>
41+
<Metrics.Section>
42+
{connector.status?.workerId && (
43+
<Metrics.Indicator label="Worker">
44+
{connector.status.workerId}
45+
</Metrics.Indicator>
46+
)}
47+
<Metrics.Indicator label="Type">{connector.type}</Metrics.Indicator>
48+
{connector.config['connector.class'] && (
49+
<Metrics.Indicator label="Class">
50+
{connector.config['connector.class']}
51+
</Metrics.Indicator>
52+
)}
53+
<Metrics.Indicator label="State">
54+
<C.Tag
55+
color={getTagColor(connector.status.state)}
56+
clickable={canShowTrace(connector)}
57+
onClick={handleStateClick}
58+
>
59+
{connector.status.state}
60+
</C.Tag>
2961
</Metrics.Indicator>
30-
)}
31-
<Metrics.Indicator label="Type">{connector.type}</Metrics.Indicator>
32-
{connector.config['connector.class'] && (
33-
<Metrics.Indicator label="Class">
34-
{connector.config['connector.class']}
62+
<Metrics.Indicator label="Tasks Running">{running}</Metrics.Indicator>
63+
<Metrics.Indicator
64+
label="Tasks Failed"
65+
isAlert
66+
alertType={failed > 0 ? 'error' : 'success'}
67+
>
68+
{failed}
3569
</Metrics.Indicator>
36-
)}
37-
<Metrics.Indicator label="State">
38-
<C.Tag color={getTagColor(connector.status.state)}>
39-
{connector.status.state}
40-
</C.Tag>
41-
</Metrics.Indicator>
42-
<Metrics.Indicator label="Tasks Running">{running}</Metrics.Indicator>
43-
<Metrics.Indicator
44-
label="Tasks Failed"
45-
isAlert
46-
alertType={failed > 0 ? 'error' : 'success'}
70+
</Metrics.Section>
71+
</Metrics.Wrapper>
72+
73+
{showTraceModal && (
74+
<Modal
75+
isOpen={showTraceModal}
76+
onClose={() => setShowTraceModal(false)}
77+
title="Connector Error Details"
78+
footer={
79+
<Button
80+
buttonType="primary"
81+
buttonSize="M"
82+
onClick={() => setShowTraceModal(false)}
83+
>
84+
Close
85+
</Button>
86+
}
4787
>
48-
{failed}
49-
</Metrics.Indicator>
50-
</Metrics.Section>
51-
</Metrics.Wrapper>
88+
{connector.status.workerId && (
89+
<S.WorkerInfo>Worker: {connector.status.workerId}</S.WorkerInfo>
90+
)}
91+
92+
{connector.status.trace && (
93+
<S.TraceContent>
94+
<div>{connector.status.trace}</div>
95+
</S.TraceContent>
96+
)}
97+
</Modal>
98+
)}
99+
</>
52100
);
53101
};
54102

frontend/src/components/Connect/Details/Overview/__tests__/Overview.spec.tsx

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import React from 'react';
22
import Overview from 'components/Connect/Details/Overview/Overview';
33
import { connector, tasks } from 'lib/fixtures/kafkaConnect';
4-
import { screen } from '@testing-library/react';
4+
import { screen, fireEvent } from '@testing-library/react';
55
import { render } from 'lib/testHelpers';
66
import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
7+
import { ConnectorState } from 'generated-sources';
78

89
jest.mock('lib/hooks/api/kafkaConnect', () => ({
910
useConnector: jest.fn(),
@@ -53,5 +54,96 @@ describe('Overview', () => {
5354
expect(screen.getByText('Tasks Failed')).toBeInTheDocument();
5455
expect(screen.getByText(1)).toBeInTheDocument();
5556
});
57+
58+
it('opens modal when FAILED state is clicked and has connector trace', () => {
59+
const failedConnector = {
60+
...connector,
61+
status: {
62+
...connector.status,
63+
state: ConnectorState.FAILED,
64+
trace: 'Test error trace',
65+
},
66+
};
67+
68+
(useConnector as jest.Mock).mockImplementation(() => ({
69+
data: failedConnector,
70+
}));
71+
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
72+
data: [],
73+
}));
74+
75+
render(<Overview />);
76+
77+
const stateTag = screen.getByText('FAILED');
78+
expect(stateTag).toBeInTheDocument();
79+
expect(stateTag).toHaveStyle('cursor: pointer');
80+
81+
fireEvent.click(stateTag);
82+
83+
expect(screen.getByText('Connector Error Details')).toBeInTheDocument();
84+
expect(screen.getByText('Test error trace')).toBeInTheDocument();
85+
});
86+
87+
it('does not open modal when FAILED state is clicked but no trace info', () => {
88+
const failedConnector = {
89+
...connector,
90+
status: {
91+
...connector.status,
92+
state: ConnectorState.FAILED,
93+
// No trace info
94+
},
95+
};
96+
97+
(useConnector as jest.Mock).mockImplementation(() => ({
98+
data: failedConnector,
99+
}));
100+
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
101+
data: [],
102+
}));
103+
104+
render(<Overview />);
105+
106+
const stateTag = screen.getByText('FAILED');
107+
expect(stateTag).toBeInTheDocument();
108+
expect(stateTag).toHaveStyle('cursor: default');
109+
110+
fireEvent.click(stateTag);
111+
112+
expect(
113+
screen.queryByText('Connector Error Details')
114+
).not.toBeInTheDocument();
115+
});
116+
117+
it('closes modal when close button is clicked', () => {
118+
const failedConnector = {
119+
...connector,
120+
status: {
121+
...connector.status,
122+
state: ConnectorState.FAILED,
123+
trace: 'Test error trace',
124+
},
125+
};
126+
127+
(useConnector as jest.Mock).mockImplementation(() => ({
128+
data: failedConnector,
129+
}));
130+
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
131+
data: [],
132+
}));
133+
134+
render(<Overview />);
135+
136+
const stateTag = screen.getByText('FAILED');
137+
fireEvent.click(stateTag);
138+
139+
expect(screen.getByText('Connector Error Details')).toBeInTheDocument();
140+
141+
const closeButton = screen.getByText('Close');
142+
fireEvent.click(closeButton);
143+
144+
expect(
145+
screen.queryByText('Connector Error Details')
146+
).not.toBeInTheDocument();
147+
});
56148
});
57149
});

0 commit comments

Comments
 (0)