Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ void toKafkaConnect() {

ConnectorDTO connectorDto = new ConnectorDTO();
connectorDto.setName(UUID.randomUUID().toString());

String traceMessage = connectorState == ConnectorStateDTO.FAILED
? "Test error trace for failed connector"
: null;

connectorDto.setStatus(
new ConnectorStatusDTO(connectorState, UUID.randomUUID().toString())
new ConnectorStatusDTO(connectorState, UUID.randomUUID().toString(), traceMessage)
);

List<TaskDTO> tasks = new ArrayList<>();
Expand Down
1 change: 1 addition & 0 deletions contract-typespec/api/kafka-connect.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ enum ConnectorState {
model ConnectorStatus {
state: ConnectorState;
workerId?: string;
trace?: string;
}

model Connector {
Expand Down
2 changes: 2 additions & 0 deletions contract/src/main/resources/swagger/kafbat-ui-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3673,6 +3673,8 @@ components:
$ref: '#/components/schemas/ConnectorState'
workerId:
type: string
trace:
type: string
required:
- state

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import styled, { css } from 'styled-components';

export const ModalOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${({ theme }) => theme.modal.overlay};
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
`;

export const ModalContent = styled.div(
({ theme: { modal } }) => css`
background-color: ${modal.backgroundColor};
color: ${modal.color};
border-radius: 8px;
padding: 24px;
max-width: 65vw;
max-height: 80vh;
overflow: auto;
position: relative;
border: 1px solid ${modal.border.contrast};
box-shadow: 0 4px 20px ${modal.shadow};
`
);

export const ModalHeader = styled.div(
({ theme: { modal } }) => css`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid ${modal.border.bottom};
padding-bottom: 12px;
`
);

export const ModalTitle = styled.h3`
margin: 0;
font-size: 18px;
font-weight: 600;
`;

export const WorkerInfo = styled.p(
({ theme: { modal } }) => css`
margin: 4px 0 0 0;
font-size: 14px;
color: ${modal.contentColor};
`
);

export const TraceContent = styled.div(
({ theme: { modal } }) => css`
background-color: ${modal.border.contrast};
padding: 16px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
color: ${modal.color};
border: 1px solid ${modal.border.contrast};
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
`
);

export const ModalFooter = styled.div(
({ theme: { modal } }) => css`
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid ${modal.border.top};
text-align: center;
display: flex;
justify-content: center;
`
);
113 changes: 86 additions & 27 deletions frontend/src/components/Connect/Details/Overview/Overview.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React from 'react';
import React, { useState } from 'react';
import * as C from 'components/common/Tag/Tag.styled';
import * as Metrics from 'components/common/Metrics';
import { Button } from 'components/common/Button/Button';
import getTagColor from 'components/common/Tag/getTagColor';
import { RouterParamsClusterConnectConnector } from 'lib/paths';
import useAppParams from 'lib/hooks/useAppParams';
import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
import { ConnectorState } from 'generated-sources';

import getTaskMetrics from './getTaskMetrics';
import * as S from './Overview.styled';

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

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

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

const hasTraceInfo = connector.status.trace;

const handleStateClick = () => {
if (connector.status.state === ConnectorState.FAILED && hasTraceInfo) {
setShowTraceModal(true);
}
};

return (
<Metrics.Wrapper>
<Metrics.Section>
{connector.status?.workerId && (
<Metrics.Indicator label="Worker">
{connector.status.workerId}
<>
<Metrics.Wrapper>
<Metrics.Section>
{connector.status?.workerId && (
<Metrics.Indicator label="Worker">
{connector.status.workerId}
</Metrics.Indicator>
)}
<Metrics.Indicator label="Type">{connector.type}</Metrics.Indicator>
{connector.config['connector.class'] && (
<Metrics.Indicator label="Class">
{connector.config['connector.class']}
</Metrics.Indicator>
)}
<Metrics.Indicator label="State">
<C.Tag
color={getTagColor(connector.status.state)}
style={{
cursor:
connector.status.state === ConnectorState.FAILED &&
hasTraceInfo
? 'pointer'
: 'default',
}}
onClick={handleStateClick}
>
{connector.status.state}
</C.Tag>
</Metrics.Indicator>
)}
<Metrics.Indicator label="Type">{connector.type}</Metrics.Indicator>
{connector.config['connector.class'] && (
<Metrics.Indicator label="Class">
{connector.config['connector.class']}
<Metrics.Indicator label="Tasks Running">{running}</Metrics.Indicator>
<Metrics.Indicator
label="Tasks Failed"
isAlert
alertType={failed > 0 ? 'error' : 'success'}
>
{failed}
</Metrics.Indicator>
)}
<Metrics.Indicator label="State">
<C.Tag color={getTagColor(connector.status.state)}>
{connector.status.state}
</C.Tag>
</Metrics.Indicator>
<Metrics.Indicator label="Tasks Running">{running}</Metrics.Indicator>
<Metrics.Indicator
label="Tasks Failed"
isAlert
alertType={failed > 0 ? 'error' : 'success'}
>
{failed}
</Metrics.Indicator>
</Metrics.Section>
</Metrics.Wrapper>
</Metrics.Section>
</Metrics.Wrapper>

{showTraceModal && (
<S.ModalOverlay onClick={() => setShowTraceModal(false)}>
<S.ModalContent
onClick={(e: React.MouseEvent) => e.stopPropagation()}
>
<S.ModalHeader>
<div>
<S.ModalTitle>Connector Error Details</S.ModalTitle>
{connector.status.workerId && (
<S.WorkerInfo>
Worker: {connector.status.workerId}
</S.WorkerInfo>
)}
</div>
</S.ModalHeader>

<S.TraceContent>
{connector.status.trace ? (
<div>{connector.status.trace}</div>
) : null}
</S.TraceContent>

<S.ModalFooter>
<Button
buttonType="primary"
buttonSize="M"
onClick={() => setShowTraceModal(false)}
>
Close
</Button>
</S.ModalFooter>
</S.ModalContent>
</S.ModalOverlay>
)}
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import Overview from 'components/Connect/Details/Overview/Overview';
import { connector, tasks } from 'lib/fixtures/kafkaConnect';
import { screen } from '@testing-library/react';
import { screen, fireEvent } from '@testing-library/react';
import { render } from 'lib/testHelpers';
import { useConnector, useConnectorTasks } from 'lib/hooks/api/kafkaConnect';
import { ConnectorState } from 'generated-sources';

jest.mock('lib/hooks/api/kafkaConnect', () => ({
useConnector: jest.fn(),
Expand Down Expand Up @@ -53,5 +54,96 @@ describe('Overview', () => {
expect(screen.getByText('Tasks Failed')).toBeInTheDocument();
expect(screen.getByText(1)).toBeInTheDocument();
});

it('opens modal when FAILED state is clicked and has connector trace', () => {
const failedConnector = {
...connector,
status: {
...connector.status,
state: ConnectorState.FAILED,
trace: 'Test error trace',
},
};

(useConnector as jest.Mock).mockImplementation(() => ({
data: failedConnector,
}));
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
data: [],
}));

render(<Overview />);

const stateTag = screen.getByText('FAILED');
expect(stateTag).toBeInTheDocument();
expect(stateTag).toHaveStyle('cursor: pointer');

fireEvent.click(stateTag);

expect(screen.getByText('Connector Error Details')).toBeInTheDocument();
expect(screen.getByText('Test error trace')).toBeInTheDocument();
});

it('does not open modal when FAILED state is clicked but no trace info', () => {
const failedConnector = {
...connector,
status: {
...connector.status,
state: ConnectorState.FAILED,
// No trace info
},
};

(useConnector as jest.Mock).mockImplementation(() => ({
data: failedConnector,
}));
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
data: [],
}));

render(<Overview />);

const stateTag = screen.getByText('FAILED');
expect(stateTag).toBeInTheDocument();
expect(stateTag).toHaveStyle('cursor: default');

fireEvent.click(stateTag);

expect(
screen.queryByText('Connector Error Details')
).not.toBeInTheDocument();
});

it('closes modal when close button is clicked', () => {
const failedConnector = {
...connector,
status: {
...connector.status,
state: ConnectorState.FAILED,
trace: 'Test error trace',
},
};

(useConnector as jest.Mock).mockImplementation(() => ({
data: failedConnector,
}));
(useConnectorTasks as jest.Mock).mockImplementation(() => ({
data: [],
}));

render(<Overview />);

const stateTag = screen.getByText('FAILED');
fireEvent.click(stateTag);

expect(screen.getByText('Connector Error Details')).toBeInTheDocument();

const closeButton = screen.getByText('Close');
fireEvent.click(closeButton);

expect(
screen.queryByText('Connector Error Details')
).not.toBeInTheDocument();
});
});
});
Loading