Skip to content

Commit 930e763

Browse files
committed
Wizard: Support Editing serdes
1 parent 8146e71 commit 930e763

File tree

7 files changed

+287
-1
lines changed

7 files changed

+287
-1
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,15 @@ export const Error = styled.p`
7979
color: ${({ theme }) => theme.input.error};
8080
font-size: 12px;
8181
`;
82+
83+
// Serde
84+
export const SerdeProperties = styled.div`
85+
display: flex;
86+
gap: 8px;
87+
`;
88+
89+
export const SerdePropertiesActions = styled(IconButtonWrapper)`
90+
align-self: stretch;
91+
margin-top: 12px;
92+
margin-left: 8px;
93+
`;
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';
@@ -140,6 +141,8 @@ const ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({
140141
<hr />
141142
<SchemaRegistry />
142143
<hr />
144+
<Serdes />
145+
<hr />
143146
<KafkaConnect />
144147
<hr />
145148
<KSQL />

frontend/src/widgets/ClusterConfigForm/schema.ts

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

48+
const serdeSchema = object({
49+
name: requiredString,
50+
className: requiredString,
51+
filePath: requiredString,
52+
topicKeysPattern: requiredString,
53+
topicValuesPattern: requiredString,
54+
properties: array().of(
55+
object({
56+
key: requiredString,
57+
value: requiredString,
58+
})
59+
),
60+
});
61+
62+
const serdesSchema = lazy((value) => {
63+
if (Array.isArray(value)) {
64+
return array().of(serdeSchema);
65+
}
66+
return mixed().optional();
67+
});
68+
4869
const kafkaConnectSchema = object({
4970
name: requiredString,
5071
address: requiredString,
@@ -255,6 +276,7 @@ const formSchema = object({
255276
auth: authSchema,
256277
schemaRegistry: urlWithAuthSchema,
257278
ksql: urlWithAuthSchema,
279+
serde: serdesSchema,
258280
kafkaConnect: kafkaConnectsSchema,
259281
masking: maskingsSchema,
260282
metrics: metricsSchema,

frontend/src/widgets/ClusterConfigForm/types.ts

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

28+
export type Serde = {
29+
name?: string;
30+
className?: string;
31+
filePath?: string;
32+
topicKeysPattern?: string;
33+
topicValuesPattern?: string;
34+
properties: {
35+
key: string;
36+
value: string;
37+
}[];
38+
};
39+
2840
type KafkaConnect = WithAuth &
2941
WithKeystore & {
3042
name: string;
@@ -55,6 +67,7 @@ export type ClusterConfigFormValues = {
5567
schemaRegistry?: URLWithAuth;
5668
ksql?: URLWithAuth;
5769
properties?: Record<string, string>;
70+
serde?: Serde[];
5871
kafkaConnect?: KafkaConnect[];
5972
metrics?: Metrics;
6073
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
) => {
@@ -44,6 +50,7 @@ export const getInitialFormData = (
4450
ksqldbServerAuth,
4551
ksqldbServerSsl,
4652
masking,
53+
serde,
4754
} = payload;
4855

4956
const initialValues: Partial<ClusterConfigFormValues> = {
@@ -82,6 +89,17 @@ export const getInitialFormData = (
8289
};
8390
}
8491

92+
if (serde && serde.length > 0) {
93+
initialValues.serde = serde.map((c) => ({
94+
name: c.name,
95+
className: c.className,
96+
filePath: c.filePath,
97+
properties: parseProperties(c.properties),
98+
topicKeysPattern: c.topicKeysPattern,
99+
topicValuesPattern: c.topicValuesPattern,
100+
}));
101+
}
102+
85103
if (kafkaConnect && kafkaConnect.length > 0) {
86104
initialValues.kafkaConnect = kafkaConnect.map((c) => ({
87105
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)