Skip to content

Commit 35159ae

Browse files
Mehmetcan Güleşçimehmetcangulesci
authored andcommitted
feat(connector): show failure modal when connector (not only tasks) is FAILED
1 parent 3a7b70f commit 35159ae

File tree

6 files changed

+269
-29
lines changed

6 files changed

+269
-29
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<>();

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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import styled, { css } from 'styled-components';
2+
3+
export const ModalOverlay = styled.div`
4+
position: fixed;
5+
top: 0;
6+
left: 0;
7+
right: 0;
8+
bottom: 0;
9+
background-color: ${({ theme }) => theme.modal.overlay};
10+
display: flex;
11+
align-items: center;
12+
justify-content: center;
13+
z-index: 1000;
14+
`;
15+
16+
export const ModalContent = styled.div(
17+
({ theme: { modal } }) => css`
18+
background-color: ${modal.backgroundColor};
19+
color: ${modal.color};
20+
border-radius: 8px;
21+
padding: 24px;
22+
max-width: 65vw;
23+
max-height: 80vh;
24+
overflow: auto;
25+
position: relative;
26+
border: 1px solid ${modal.border.contrast};
27+
box-shadow: 0 4px 20px ${modal.shadow};
28+
`
29+
);
30+
31+
export const ModalHeader = styled.div(
32+
({ theme: { modal } }) => css`
33+
display: flex;
34+
justify-content: space-between;
35+
align-items: center;
36+
margin-bottom: 16px;
37+
border-bottom: 1px solid ${modal.border.bottom};
38+
padding-bottom: 12px;
39+
`
40+
);
41+
42+
export const ModalTitle = styled.h3`
43+
margin: 0;
44+
font-size: 18px;
45+
font-weight: 600;
46+
`;
47+
48+
export const WorkerInfo = styled.p(
49+
({ theme: { modal } }) => css`
50+
margin: 4px 0 0 0;
51+
font-size: 14px;
52+
color: ${modal.contentColor};
53+
`
54+
);
55+
56+
export const TraceContent = styled.div(
57+
({ theme: { modal } }) => css`
58+
background-color: ${modal.border.contrast};
59+
padding: 16px;
60+
border-radius: 6px;
61+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
62+
font-size: 12px;
63+
color: ${modal.color};
64+
border: 1px solid ${modal.border.contrast};
65+
max-height: 400px;
66+
overflow-y: auto;
67+
white-space: pre-wrap;
68+
word-break: break-word;
69+
`
70+
);
71+
72+
export const ModalFooter = styled.div(
73+
({ theme: { modal } }) => css`
74+
margin-top: 16px;
75+
padding-top: 12px;
76+
border-top: 1px solid ${modal.border.top};
77+
text-align: center;
78+
display: flex;
79+
justify-content: center;
80+
`
81+
);

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

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
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';
45
import getTagColor from 'components/common/Tag/getTagColor';
56
import { RouterParamsClusterConnectConnector } from 'lib/paths';
67
import useAppParams from 'lib/hooks/useAppParams';
78
import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
9+
import { ConnectorState } from 'generated-sources';
810

911
import getTaskMetrics from './getTaskMetrics';
12+
import * as S from './Overview.styled';
1013

1114
const Overview: React.FC = () => {
1215
const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
16+
const [showTraceModal, setShowTraceModal] = useState(false);
1317

1418
const { data: connector } = useConnector(routerProps);
1519
const { data: tasks } = useConnectorTasks(routerProps);
@@ -20,35 +24,90 @@ const Overview: React.FC = () => {
2024

2125
const { running, failed } = getTaskMetrics(tasks);
2226

27+
const hasTraceInfo = connector.status.trace;
28+
29+
const handleStateClick = () => {
30+
if (connector.status.state === ConnectorState.FAILED && hasTraceInfo) {
31+
setShowTraceModal(true);
32+
}
33+
};
34+
2335
return (
24-
<Metrics.Wrapper>
25-
<Metrics.Section>
26-
{connector.status?.workerId && (
27-
<Metrics.Indicator label="Worker">
28-
{connector.status.workerId}
36+
<>
37+
<Metrics.Wrapper>
38+
<Metrics.Section>
39+
{connector.status?.workerId && (
40+
<Metrics.Indicator label="Worker">
41+
{connector.status.workerId}
42+
</Metrics.Indicator>
43+
)}
44+
<Metrics.Indicator label="Type">{connector.type}</Metrics.Indicator>
45+
{connector.config['connector.class'] && (
46+
<Metrics.Indicator label="Class">
47+
{connector.config['connector.class']}
48+
</Metrics.Indicator>
49+
)}
50+
<Metrics.Indicator label="State">
51+
<C.Tag
52+
color={getTagColor(connector.status.state)}
53+
style={{
54+
cursor:
55+
connector.status.state === ConnectorState.FAILED &&
56+
hasTraceInfo
57+
? 'pointer'
58+
: 'default',
59+
}}
60+
onClick={handleStateClick}
61+
>
62+
{connector.status.state}
63+
</C.Tag>
2964
</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']}
65+
<Metrics.Indicator label="Tasks Running">{running}</Metrics.Indicator>
66+
<Metrics.Indicator
67+
label="Tasks Failed"
68+
isAlert
69+
alertType={failed > 0 ? 'error' : 'success'}
70+
>
71+
{failed}
3572
</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'}
47-
>
48-
{failed}
49-
</Metrics.Indicator>
50-
</Metrics.Section>
51-
</Metrics.Wrapper>
73+
</Metrics.Section>
74+
</Metrics.Wrapper>
75+
76+
{showTraceModal && (
77+
<S.ModalOverlay onClick={() => setShowTraceModal(false)}>
78+
<S.ModalContent
79+
onClick={(e: React.MouseEvent) => e.stopPropagation()}
80+
>
81+
<S.ModalHeader>
82+
<div>
83+
<S.ModalTitle>Connector Error Details</S.ModalTitle>
84+
{connector.status.workerId && (
85+
<S.WorkerInfo>
86+
Worker: {connector.status.workerId}
87+
</S.WorkerInfo>
88+
)}
89+
</div>
90+
</S.ModalHeader>
91+
92+
<S.TraceContent>
93+
{connector.status.trace ? (
94+
<div>{connector.status.trace}</div>
95+
) : null}
96+
</S.TraceContent>
97+
98+
<S.ModalFooter>
99+
<Button
100+
buttonType="primary"
101+
buttonSize="M"
102+
onClick={() => setShowTraceModal(false)}
103+
>
104+
Close
105+
</Button>
106+
</S.ModalFooter>
107+
</S.ModalContent>
108+
</S.ModalOverlay>
109+
)}
110+
</>
52111
);
53112
};
54113

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)