Skip to content

Commit 5bb06e5

Browse files
authored
FE: Wizard: Support Editing serdes (#866)
1 parent 3048605 commit 5bb06e5

File tree

8 files changed

+276
-1
lines changed

8 files changed

+276
-1
lines changed

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

+12
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+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';
8+
import Heading from 'components/common/heading/Heading.styled';
9+
10+
const PropertiesFields = ({ nestedId }: { nestedId: number }) => {
11+
const { control } = useFormContext();
12+
const { fields, append, remove } = useFieldArray({
13+
control,
14+
name: `serde.${nestedId}.properties`,
15+
});
16+
17+
return (
18+
<S.GroupFieldWrapper>
19+
<Heading level={4}>Serde properties</Heading>
20+
{fields.map((propsField, propsIndex) => (
21+
<S.SerdeProperties key={propsField.id}>
22+
<Input
23+
name={`serde.${nestedId}.properties.${propsIndex}.key`}
24+
placeholder="Key"
25+
type="text"
26+
withError
27+
/>
28+
<Input
29+
name={`serde.${nestedId}.properties.${propsIndex}.value`}
30+
placeholder="Value"
31+
type="text"
32+
withError
33+
/>
34+
<S.SerdePropertiesActions
35+
aria-label="deleteProperty"
36+
onClick={() => remove(propsIndex)}
37+
>
38+
<CloseCircleIcon aria-hidden />
39+
</S.SerdePropertiesActions>
40+
</S.SerdeProperties>
41+
))}
42+
<div>
43+
<Button
44+
type="button"
45+
buttonSize="M"
46+
buttonType="secondary"
47+
onClick={() => append({ key: '', value: '' })}
48+
>
49+
<PlusIcon />
50+
Add Property
51+
</Button>
52+
</div>
53+
</S.GroupFieldWrapper>
54+
);
55+
};
56+
57+
export default PropertiesFields;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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 {
10+
FlexGrow1,
11+
FlexRow,
12+
} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
13+
import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';
14+
15+
import PropertiesFields from './PropertiesFields';
16+
17+
const Serdes = () => {
18+
const { control } = useFormContext();
19+
const { fields, append, remove } = useFieldArray({
20+
control,
21+
name: 'serde',
22+
});
23+
24+
const handleAppend = () =>
25+
append({
26+
name: '',
27+
className: '',
28+
filePath: '',
29+
topicKeysPattern: '%s-key',
30+
topicValuesPattern: '%s-value',
31+
});
32+
const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove());
33+
34+
const hasFields = fields.length > 0;
35+
36+
return (
37+
<>
38+
<SectionHeader
39+
title="Serdes"
40+
addButtonText="Configure Serdes"
41+
adding={!hasFields}
42+
onClick={toggleConfig}
43+
/>
44+
{hasFields && (
45+
<S.GroupFieldWrapper>
46+
{fields.map((item, index) => (
47+
<div key={item.id}>
48+
<FlexRow>
49+
<FlexGrow1>
50+
<Input
51+
label="Name *"
52+
name={`serde.${index}.name`}
53+
placeholder="Name"
54+
type="text"
55+
hint="Serde name"
56+
withError
57+
/>
58+
<Input
59+
label="Class Name *"
60+
name={`serde.${index}.className`}
61+
placeholder="className"
62+
type="text"
63+
hint="Serde class name"
64+
withError
65+
/>
66+
<Input
67+
label="File Path *"
68+
name={`serde.${index}.filePath`}
69+
placeholder="serde file path"
70+
type="text"
71+
hint="Serde file path"
72+
withError
73+
/>
74+
<Input
75+
label="Topic Keys Pattern *"
76+
name={`serde.${index}.topicKeysPattern`}
77+
placeholder="topicKeysPattern"
78+
type="text"
79+
hint="Serde topic keys pattern"
80+
withError
81+
/>
82+
<Input
83+
label="Topic Values Pattern *"
84+
name={`serde.${index}.topicValuesPattern`}
85+
placeholder="topicValuesPattern"
86+
type="text"
87+
hint="Serde topic values pattern"
88+
withError
89+
/>
90+
<hr />
91+
<PropertiesFields nestedId={index} />
92+
</FlexGrow1>
93+
<S.RemoveButton onClick={() => remove(index)}>
94+
<IconButtonWrapper aria-label="deleteProperty">
95+
<CloseCircleIcon aria-hidden />
96+
</IconButtonWrapper>
97+
</S.RemoveButton>
98+
</FlexRow>
99+
100+
<hr />
101+
</div>
102+
))}
103+
<Button
104+
type="button"
105+
buttonSize="M"
106+
buttonType="secondary"
107+
onClick={handleAppend}
108+
>
109+
<PlusIcon />
110+
Add Serde
111+
</Button>
112+
</S.GroupFieldWrapper>
113+
)}
114+
</>
115+
);
116+
};
117+
export default Serdes;

frontend/src/widgets/ClusterConfigForm/index.tsx

+3
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/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

+22
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

+13
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

+18
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

+34-1
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)