Skip to content

Commit 17f4125

Browse files
committed
Wizard: Support Editing serdes
1 parent 49894b8 commit 17f4125

File tree

7 files changed

+288
-2
lines changed

7 files changed

+288
-2
lines changed

frontend/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,17 @@ export const FileUploadInputWrapper = styled.div`
5858
display: flex;
5959
height: 40px;
6060
align-items: center;
61-
color: ${({ theme }) => theme.clusterConfigForm.fileInput.color}};
61+
color: ${({ theme }) => theme.clusterConfigForm.fileInput.color};
62+
`;
63+
64+
// Serde
65+
export const SerdeProperties = styled.div`
66+
display: flex;
67+
gap: 8px;
68+
`;
69+
70+
export const SerdePropertiesActions = styled(IconButtonWrapper)`
71+
align-self: stretch;
72+
margin-top: 12px;
73+
margin-left: 8px;
6274
`;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import * as React from 'react';
2+
import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
3+
import { Button } from 'components/common/Button/Button';
4+
import Input from 'components/common/Input/Input';
5+
import { useFieldArray, useFormContext } from 'react-hook-form';
6+
import PlusIcon from 'components/common/Icons/PlusIcon';
7+
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
8+
import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';
9+
import Heading from 'components/common/heading/Heading.styled';
10+
import {
11+
FlexGrow1,
12+
FlexRow,
13+
} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
14+
import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';
15+
import { Serde } from 'widgets/ClusterConfigForm/types';
16+
17+
const Serdes = () => {
18+
const { control, reset, getValues } = useFormContext();
19+
const { fields, append, remove } = useFieldArray({
20+
control,
21+
name: 'serde',
22+
});
23+
const {
24+
fields: propsFields,
25+
append: appendProps,
26+
remove: removeProps,
27+
} = useFieldArray({
28+
control,
29+
name: 'properties',
30+
});
31+
32+
React.useEffect(() => {
33+
reset();
34+
getValues().serde?.forEach((item: Serde, index: number) => {
35+
item.properties?.forEach((itemProp) => {
36+
appendProps({
37+
key: itemProp.key,
38+
value: itemProp.value,
39+
serdeIndex: index,
40+
});
41+
});
42+
});
43+
}, []);
44+
45+
const handleAppend = () =>
46+
append({
47+
name: '',
48+
className: '',
49+
filePath: '',
50+
topicKeysPattern: '%s-key',
51+
topicValuesPattern: '%s-value',
52+
});
53+
const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove());
54+
55+
const hasFields = fields.length > 0;
56+
57+
return (
58+
<>
59+
<SectionHeader
60+
title="Serdes"
61+
addButtonText="Configure Serdes"
62+
adding={!hasFields}
63+
onClick={toggleConfig}
64+
/>
65+
{hasFields && (
66+
<S.GroupFieldWrapper>
67+
{fields.map((item, index) => (
68+
<div key={item.id}>
69+
<FlexRow>
70+
<FlexGrow1>
71+
<Input
72+
label="Name *"
73+
name={`serde.${index}.name`}
74+
placeholder="Name"
75+
type="text"
76+
hint="Serde name"
77+
withError
78+
/>
79+
<Input
80+
label="Class Name *"
81+
name={`serde.${index}.className`}
82+
placeholder="className"
83+
type="text"
84+
hint="Serde class name"
85+
withError
86+
/>
87+
<Input
88+
label="File Path *"
89+
name={`serde.${index}.filePath`}
90+
placeholder="serde file path"
91+
type="text"
92+
hint="Serde file path"
93+
withError
94+
/>
95+
<Input
96+
label="Topic Keys Pattern *"
97+
name={`serde.${index}.topicKeysPattern`}
98+
placeholder="topicKeysPattern"
99+
type="text"
100+
hint="Serde topic keys pattern"
101+
withError
102+
/>
103+
<Input
104+
label="Topic Values Pattern *"
105+
name={`serde.${index}.topicValuesPattern`}
106+
placeholder="topicValuesPattern"
107+
type="text"
108+
hint="Serde topic values pattern"
109+
withError
110+
/>
111+
<hr />
112+
<S.GroupFieldWrapper>
113+
<Heading level={4}>Serde properties</Heading>
114+
{propsFields
115+
.filter(
116+
(propItem) =>
117+
(propItem as unknown as { serdeIndex: number })
118+
.serdeIndex === index
119+
)
120+
.map((propsField, propsIndex) => (
121+
<S.SerdeProperties key={propsField.id}>
122+
<div>
123+
<Input
124+
name={`serde.${index}.properties.${propsIndex}.key`}
125+
placeholder="Key"
126+
type="text"
127+
withError
128+
/>
129+
</div>
130+
<div>
131+
<Input
132+
name={`serde.${index}.properties.${propsIndex}.value`}
133+
placeholder="Value"
134+
type="text"
135+
withError
136+
/>
137+
</div>
138+
<S.SerdePropertiesActions
139+
aria-label="deleteProperty"
140+
onClick={() => removeProps(index)}
141+
>
142+
<CloseCircleIcon aria-hidden />
143+
</S.SerdePropertiesActions>
144+
</S.SerdeProperties>
145+
))}
146+
<div>
147+
<Button
148+
type="button"
149+
buttonSize="M"
150+
buttonType="secondary"
151+
onClick={() =>
152+
appendProps({ key: '', value: '', serdeIndex: index })
153+
}
154+
>
155+
<PlusIcon />
156+
Add Property
157+
</Button>
158+
</div>
159+
</S.GroupFieldWrapper>
160+
</FlexGrow1>
161+
<S.RemoveButton onClick={() => remove(index)}>
162+
<IconButtonWrapper aria-label="deleteProperty">
163+
<CloseCircleIcon aria-hidden />
164+
</IconButtonWrapper>
165+
</S.RemoveButton>
166+
</FlexRow>
167+
168+
<hr />
169+
</div>
170+
))}
171+
<Button
172+
type="button"
173+
buttonSize="M"
174+
buttonType="secondary"
175+
onClick={handleAppend}
176+
>
177+
<PlusIcon />
178+
Add Serde
179+
</Button>
180+
</S.GroupFieldWrapper>
181+
)}
182+
</>
183+
);
184+
};
185+
export default Serdes;

frontend/src/widgets/ClusterConfigForm/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useNavigate } from 'react-router-dom';
1717
import useBoolean from 'lib/hooks/useBoolean';
1818
import KafkaCluster from 'widgets/ClusterConfigForm/Sections/KafkaCluster';
1919
import SchemaRegistry from 'widgets/ClusterConfigForm/Sections/SchemaRegistry';
20+
import Serdes from 'widgets/ClusterConfigForm/Sections/Serdes';
2021
import KafkaConnect from 'widgets/ClusterConfigForm/Sections/KafkaConnect';
2122
import Metrics from 'widgets/ClusterConfigForm/Sections/Metrics';
2223
import CustomAuthentication from 'widgets/ClusterConfigForm/Sections/CustomAuthentication';
@@ -139,6 +140,8 @@ const ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({
139140
<hr />
140141
<SchemaRegistry />
141142
<hr />
143+
<Serdes />
144+
<hr />
142145
<KafkaConnect />
143146
<hr />
144147
<KSQL />

frontend/src/widgets/ClusterConfigForm/schema.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ const urlWithAuthSchema = lazy((value) => {
4444
return mixed().optional();
4545
});
4646

47+
const serdeSchema = object({
48+
name: requiredString,
49+
className: requiredString,
50+
filePath: requiredString,
51+
topicKeysPattern: requiredString,
52+
topicValuesPattern: requiredString,
53+
properties: array().of(
54+
object({
55+
key: requiredString,
56+
value: requiredString,
57+
})
58+
),
59+
});
60+
61+
const serdesSchema = lazy((value) => {
62+
if (Array.isArray(value)) {
63+
return array().of(serdeSchema);
64+
}
65+
return mixed().optional();
66+
});
67+
4768
const kafkaConnectSchema = object({
4869
name: requiredString,
4970
address: requiredString,
@@ -189,6 +210,7 @@ const formSchema = object({
189210
auth: authSchema,
190211
schemaRegistry: urlWithAuthSchema,
191212
ksql: urlWithAuthSchema,
213+
serde: serdesSchema,
192214
kafkaConnect: kafkaConnectsSchema,
193215
metrics: metricsSchema,
194216
});

frontend/src/widgets/ClusterConfigForm/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ type URLWithAuth = WithAuth &
2323
isActive?: string;
2424
};
2525

26+
export type Serde = {
27+
name?: string;
28+
className?: string;
29+
filePath?: string;
30+
topicKeysPattern?: string;
31+
topicValuesPattern?: string;
32+
properties: {
33+
key: string;
34+
value: string;
35+
}[];
36+
};
37+
2638
type KafkaConnect = WithAuth &
2739
WithKeystore & {
2840
name: string;
@@ -53,6 +65,7 @@ export type ClusterConfigFormValues = {
5365
schemaRegistry?: URLWithAuth;
5466
ksql?: URLWithAuth;
5567
properties?: Record<string, string>;
68+
serde?: Serde[];
5669
kafkaConnect?: KafkaConnect[];
5770
metrics?: Metrics;
5871
customAuth: Record<string, string>;

frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ const parseCredentials = (username?: string, password?: string) => {
3030
return { isAuth: true, username, password };
3131
};
3232

33+
const parseProperties = (properties?: { [key: string]: string }) =>
34+
Object.entries(properties || {}).map(([key, value]) => ({
35+
key,
36+
value,
37+
}));
38+
3339
export const getInitialFormData = (
3440
payload: ApplicationConfigPropertiesKafkaClusters
3541
) => {
@@ -43,6 +49,7 @@ export const getInitialFormData = (
4349
ksqldbServer,
4450
ksqldbServerAuth,
4551
ksqldbServerSsl,
52+
serde,
4653
} = payload;
4754

4855
const initialValues: Partial<ClusterConfigFormValues> = {
@@ -81,6 +88,17 @@ export const getInitialFormData = (
8188
};
8289
}
8390

91+
if (serde && serde.length > 0) {
92+
initialValues.serde = serde.map((c) => ({
93+
name: c.name,
94+
className: c.className,
95+
filePath: c.filePath,
96+
properties: parseProperties(c.properties),
97+
topicKeysPattern: c.topicKeysPattern,
98+
topicValuesPattern: c.topicValuesPattern,
99+
}));
100+
}
101+
84102
if (kafkaConnect && kafkaConnect.length > 0) {
85103
initialValues.kafkaConnect = kafkaConnect.map((c) => ({
86104
name: c.name as string,

frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types';
1+
import {
2+
ClusterConfigFormValues,
3+
Serde,
4+
} from 'widgets/ClusterConfigForm/types';
25
import { ApplicationConfigPropertiesKafkaClusters } from 'generated-sources';
36

47
import { getJaasConfig } from './getJaasConfig';
@@ -35,6 +38,15 @@ const transformCustomProps = (props: Record<string, string>) => {
3538
return config;
3639
};
3740

41+
const transformSerdeProperties = (properties: Serde['properties']) => {
42+
const mappedProperties: { [key: string]: string } = {};
43+
44+
properties.forEach(({ key, value }) => {
45+
mappedProperties[key] = value;
46+
});
47+
return mappedProperties;
48+
};
49+
3850
export const transformFormDataToPayload = (data: ClusterConfigFormValues) => {
3951
const config: ApplicationConfigPropertiesKafkaClusters = {
4052
name: data.name,
@@ -75,6 +87,27 @@ export const transformFormDataToPayload = (data: ClusterConfigFormValues) => {
7587
config.ksqldbServerSsl = transformToKeystore(data.ksql.keystore);
7688
}
7789

90+
// Serde
91+
if (data.serde && data.serde.length > 0) {
92+
config.serde = data.serde.map(
93+
({
94+
name,
95+
className,
96+
filePath,
97+
topicKeysPattern,
98+
topicValuesPattern,
99+
properties,
100+
}) => ({
101+
name,
102+
className,
103+
filePath,
104+
topicKeysPattern,
105+
topicValuesPattern,
106+
properties: transformSerdeProperties(properties),
107+
})
108+
);
109+
}
110+
78111
// Kafka Connect
79112
if (data.kafkaConnect && data.kafkaConnect.length > 0) {
80113
config.kafkaConnect = data.kafkaConnect.map(

0 commit comments

Comments
 (0)