Skip to content

Commit 34ff202

Browse files
authored
Merge branch 'main' into issue/233
2 parents 02d7f32 + 5079b00 commit 34ff202

File tree

31 files changed

+626
-87
lines changed

31 files changed

+626
-87
lines changed

api/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ dependencies {
6565
// CVE Fixes
6666
implementation libs.apache.commons.compress
6767
implementation libs.okhttp3.logging.intercepter
68+
implementation libs.reactor.netty.http
69+
// CVE Fixes End
6870

6971
implementation libs.modelcontextprotocol.spring.webflux
7072
implementation libs.victools.jsonschema.generator

api/src/main/java/io/kafbat/ui/serdes/ProducerRecordCreator.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import javax.annotation.Nullable;
66
import lombok.RequiredArgsConstructor;
77
import org.apache.kafka.clients.producer.ProducerRecord;
8-
import org.apache.kafka.common.header.Header;
8+
import org.apache.kafka.common.header.Headers;
99
import org.apache.kafka.common.header.internals.RecordHeader;
1010
import org.apache.kafka.common.header.internals.RecordHeaders;
1111

@@ -20,18 +20,23 @@ public ProducerRecord<byte[], byte[]> create(String topic,
2020
@Nullable String key,
2121
@Nullable String value,
2222
@Nullable Map<String, String> headers) {
23+
24+
Headers kafkaHeaders = createHeaders(headers);
25+
2326
return new ProducerRecord<>(
2427
topic,
2528
partition,
26-
key == null ? null : keySerializer.serialize(key),
27-
value == null ? null : valuesSerializer.serialize(value),
28-
headers == null ? null : createHeaders(headers)
29+
key == null ? null : keySerializer.serialize(key, kafkaHeaders),
30+
value == null ? null : valuesSerializer.serialize(value, kafkaHeaders),
31+
kafkaHeaders
2932
);
3033
}
3134

32-
private Iterable<Header> createHeaders(Map<String, String> clientHeaders) {
35+
private Headers createHeaders(Map<String, String> clientHeaders) {
3336
RecordHeaders headers = new RecordHeaders();
34-
clientHeaders.forEach((k, v) -> headers.add(new RecordHeader(k, v == null ? null : v.getBytes())));
37+
if (clientHeaders != null) {
38+
clientHeaders.forEach((k, v) -> headers.add(new RecordHeader(k, v == null ? null : v.getBytes())));
39+
}
3540
return headers;
3641
}
3742

api/src/main/java/io/kafbat/ui/serdes/SerdeInstance.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import lombok.Getter;
1111
import lombok.RequiredArgsConstructor;
1212
import lombok.extern.slf4j.Slf4j;
13+
import org.apache.kafka.common.header.Headers;
1314

1415
@Slf4j
1516
@RequiredArgsConstructor
@@ -80,7 +81,17 @@ public boolean canDeserialize(String topic, Serde.Target type) {
8081
public Serde.Serializer serializer(String topic, Serde.Target type) {
8182
return wrapWithClassloader(() -> {
8283
var serializer = serde.serializer(topic, type);
83-
return input -> wrapWithClassloader(() -> serializer.serialize(input));
84+
return new Serde.Serializer() {
85+
@Override
86+
public byte[] serialize(String input) {
87+
return wrapWithClassloader(() -> serializer.serialize(input));
88+
}
89+
90+
@Override
91+
public byte[] serialize(String input, Headers headers) {
92+
return wrapWithClassloader(() -> serializer.serialize(input, headers));
93+
}
94+
};
8495
});
8596
}
8697

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
import { LinkCell } from 'components/common/NewTable';
3+
import useAppParams from 'lib/hooks/useAppParams';
4+
import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
5+
import { CellContext } from '@tanstack/react-table';
6+
import { FullConnectorInfo } from 'generated-sources';
7+
8+
type KafkaConnectLinkCellProps = CellContext<FullConnectorInfo, string>;
9+
10+
export const KafkaConnectLinkCell = ({
11+
getValue,
12+
row,
13+
}: KafkaConnectLinkCellProps) => {
14+
const { clusterName } = useAppParams<ClusterNameRoute>();
15+
const { connect, name } = row.original;
16+
const value = getValue();
17+
18+
return (
19+
<LinkCell
20+
value={value}
21+
to={clusterConnectConnectorPath(clusterName, connect, name)}
22+
wordBreak
23+
/>
24+
);
25+
};

frontend/src/components/Connect/List/List.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import React from 'react';
22
import useAppParams from 'lib/hooks/useAppParams';
3-
import { clusterConnectConnectorPath, ClusterNameRoute } from 'lib/paths';
3+
import { ClusterNameRoute } from 'lib/paths';
44
import Table, { TagCell } from 'components/common/NewTable';
55
import { FullConnectorInfo } from 'generated-sources';
66
import { useConnectors } from 'lib/hooks/api/kafkaConnect';
77
import { ColumnDef } from '@tanstack/react-table';
8-
import { useNavigate, useSearchParams } from 'react-router-dom';
9-
import BreakableTextCell from 'components/common/NewTable/BreakableTextCell';
8+
import { useSearchParams } from 'react-router-dom';
109
import { useQueryPersister } from 'components/common/NewTable/ColumnFilter';
1110
import { useLocalStoragePersister } from 'components/common/NewTable/ColumnResizer/lib';
11+
import BreakableTextCell from 'components/common/NewTable/BreakableTextCell';
1212

1313
import ActionsCell from './ActionsCell';
1414
import TopicsCell from './TopicsCell';
1515
import RunningTasksCell from './RunningTasksCell';
16+
import { KafkaConnectLinkCell } from './KafkaConnectLinkCell';
1617

17-
const kafkaConnectColumns: ColumnDef<FullConnectorInfo>[] = [
18+
const kafkaConnectColumns: ColumnDef<FullConnectorInfo, string>[] = [
1819
{
1920
header: 'Name',
2021
accessorKey: 'name',
21-
cell: BreakableTextCell,
22+
cell: KafkaConnectLinkCell,
2223
enableResizing: true,
2324
},
2425
{
@@ -77,7 +78,6 @@ const kafkaConnectColumns: ColumnDef<FullConnectorInfo>[] = [
7778
];
7879

7980
const List: React.FC = () => {
80-
const navigate = useNavigate();
8181
const { clusterName } = useAppParams<ClusterNameRoute>();
8282
const [searchParams] = useSearchParams();
8383
const { data: connectors } = useConnectors(
@@ -95,9 +95,6 @@ const List: React.FC = () => {
9595
enableSorting
9696
enableColumnResizing
9797
columnSizingPersister={columnSizingPersister}
98-
onRowClick={({ original: { connect, name } }) =>
99-
navigate(clusterConnectConnectorPath(clusterName, connect, name))
100-
}
10198
emptyMessage="No connectors found"
10299
setRowId={(originalRow) => `${originalRow.name}-${originalRow.connect}`}
103100
filterPersister={filterPersister}

frontend/src/components/Connect/List/__tests__/List.spec.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,13 @@ describe('Connectors List', () => {
6464

6565
it('opens broker when row clicked', async () => {
6666
renderComponent();
67-
await userEvent.click(
68-
screen.getByRole('row', {
69-
name: 'hdfs-source-connector first SOURCE FileStreamSource a b c RUNNING 2 of 2',
70-
})
71-
);
72-
await waitFor(() =>
73-
expect(mockedUsedNavigate).toBeCalledWith(
74-
clusterConnectConnectorPath(
75-
clusterName,
76-
'first',
77-
'hdfs-source-connector'
78-
)
67+
screen.debug();
68+
expect(screen.getByText('hdfs-source-connector')).toHaveAttribute(
69+
'href',
70+
clusterConnectConnectorPath(
71+
clusterName,
72+
'first',
73+
'hdfs-source-connector'
7974
)
8075
);
8176
});

frontend/src/components/ConsumerGroups/Details/ResetOffsets/Form.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import useAppParams from 'lib/hooks/useAppParams';
2323
import { useResetConsumerGroupOffsetsMutation } from 'lib/hooks/api/consumers';
2424
import { FlexFieldset, StyledForm } from 'components/common/Form/Form.styled';
2525
import ControlledSelect from 'components/common/Select/ControlledSelect';
26+
import { useTimezone } from 'lib/hooks/useTimezones';
2627

2728
import * as S from './ResetOffsets.styled';
2829

@@ -38,6 +39,7 @@ const resetTypeOptions = Object.values(ConsumerGroupOffsetsResetType).map(
3839

3940
const Form: React.FC<FormProps> = ({ defaultValues, partitions, topics }) => {
4041
const navigate = useNavigate();
42+
const { getDateInCurrentTimezone } = useTimezone();
4143
const routerParams = useAppParams<ClusterGroupParam>();
4244
const reset = useResetConsumerGroupOffsetsMutation(routerParams);
4345
const topicOptions = React.useMemo(
@@ -142,8 +144,12 @@ const Form: React.FC<FormProps> = ({ defaultValues, partitions, topics }) => {
142144
render={({ field: { onChange, onBlur, value, ref } }) => (
143145
<S.DatePickerInput
144146
ref={ref}
145-
selected={new Date(value as number)}
146-
onChange={(e: Date | null) => onChange(e?.getTime())}
147+
selected={getDateInCurrentTimezone(
148+
new Date(value as number)
149+
)}
150+
onChange={(selectedDate: Date | null) => {
151+
onChange(selectedDate?.getTime());
152+
}}
147153
onBlur={onBlur}
148154
/>
149155
)}

frontend/src/components/NavBar/NavBar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import ProductHuntIcon from 'components/common/Icons/ProductHuntIcon';
1212
import { Button } from 'components/common/Button/Button';
1313
import MenuIcon from 'components/common/Icons/MenuIcon';
1414

15+
import { UserTimezone } from './UserTimezone/UserTimezone';
1516
import UserInfo from './UserInfo/UserInfo';
1617
import * as S from './NavBar.styled';
1718

@@ -73,6 +74,8 @@ const NavBar: React.FC<Props> = ({ onBurgerClick }) => {
7374
</S.NavbarBrand>
7475
</S.NavbarBrand>
7576
<S.NavbarSocial>
77+
<UserTimezone />
78+
7679
<Select
7780
options={options}
7881
value={themeMode}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import styled from 'styled-components';
2+
3+
export const SelectedTimezoneContainer = styled.div`
4+
display: flex;
5+
justify-content: center;
6+
align-items: center;
7+
gap: 4px;
8+
`;
9+
10+
export const ContentContainer = styled.div`
11+
display: flex;
12+
width: 320px;
13+
flex-direction: column;
14+
max-height: 640px;
15+
gap: 8px;
16+
`;
17+
18+
export const InputContainer = styled.div`
19+
position: sticky;
20+
top: 0;
21+
background-color: white;
22+
z-index: 1;
23+
`;
24+
25+
export const ItemsContainer = styled.div`
26+
overflow-y: auto;
27+
`;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Button } from 'components/common/Button/Button';
2+
import ChevronDownIcon from 'components/common/Icons/ChevronDownIcon';
3+
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
4+
import React, { useMemo, useState } from 'react';
5+
import Input from 'components/common/Input/Input';
6+
import { TIMEZONES, useTimezone } from 'lib/hooks/useTimezones';
7+
8+
import * as S from './UserTimezone.styled';
9+
10+
export const UserTimezone = () => {
11+
const { currentTimezone, availableTimezones, setTimezone } = useTimezone();
12+
13+
const [searchValue, setSearchValue] = useState('');
14+
15+
const filteredTimezones = useMemo(() => {
16+
if (!searchValue.trim()) return availableTimezones;
17+
18+
const searchLower = searchValue.toLowerCase();
19+
return TIMEZONES.filter(
20+
(timezone) =>
21+
timezone.value.toLowerCase().includes(searchLower) ||
22+
timezone.offset.toLowerCase().includes(searchLower)
23+
);
24+
}, [searchValue]);
25+
26+
const handleTimezoneSelect = (timezone: typeof currentTimezone) => {
27+
setTimezone(timezone);
28+
setSearchValue('');
29+
};
30+
31+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
32+
setSearchValue(e.target.value);
33+
};
34+
35+
return (
36+
<Dropdown
37+
onClose={() => setSearchValue('')}
38+
align="center"
39+
aria-label="user-timezone-dropdown"
40+
openBtnEl={
41+
<Button buttonType="text" buttonSize="L">
42+
<S.SelectedTimezoneContainer>
43+
<p>{currentTimezone.UTCOffset}</p>
44+
<ChevronDownIcon fill="currentColor" width="16" height="16" />
45+
</S.SelectedTimezoneContainer>
46+
</Button>
47+
}
48+
>
49+
<S.ContentContainer>
50+
<S.InputContainer>
51+
<Input
52+
id="user-timezone-search"
53+
type="text"
54+
placeholder="Search timezone..."
55+
value={searchValue}
56+
onChange={handleSearchChange}
57+
inputSize="M"
58+
search
59+
/>
60+
</S.InputContainer>
61+
62+
<S.ItemsContainer>
63+
{filteredTimezones.map((timezone) => (
64+
<DropdownItem
65+
key={timezone.value}
66+
onClick={() => handleTimezoneSelect(timezone)}
67+
>
68+
{timezone.label}
69+
</DropdownItem>
70+
))}
71+
</S.ItemsContainer>
72+
</S.ContentContainer>
73+
</Dropdown>
74+
);
75+
};

0 commit comments

Comments
 (0)