diff --git a/frontend/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts b/frontend/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts
index a99c2f51b..05e86ef6c 100644
--- a/frontend/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts
+++ b/frontend/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts
@@ -79,3 +79,15 @@ export const Error = styled.p`
color: ${({ theme }) => theme.input.error};
font-size: 12px;
`;
+
+// Serde
+export const SerdeProperties = styled.div`
+ display: flex;
+ gap: 8px;
+`;
+
+export const SerdePropertiesActions = styled(IconButtonWrapper)`
+ align-self: stretch;
+ margin-top: 12px;
+ margin-left: 8px;
+`;
diff --git a/frontend/src/widgets/ClusterConfigForm/Sections/Serdes/PropertiesFields.tsx b/frontend/src/widgets/ClusterConfigForm/Sections/Serdes/PropertiesFields.tsx
new file mode 100644
index 000000000..82d270c33
--- /dev/null
+++ b/frontend/src/widgets/ClusterConfigForm/Sections/Serdes/PropertiesFields.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
+import { Button } from 'components/common/Button/Button';
+import Input from 'components/common/Input/Input';
+import { useFieldArray, useFormContext } from 'react-hook-form';
+import PlusIcon from 'components/common/Icons/PlusIcon';
+import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';
+import Heading from 'components/common/heading/Heading.styled';
+
+const PropertiesFields = ({ nestedId }: { nestedId: number }) => {
+ const { control } = useFormContext();
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: `serde.${nestedId}.properties`,
+ });
+
+ return (
+
+ Serde properties
+ {fields.map((propsField, propsIndex) => (
+
+
+
+ remove(propsIndex)}
+ >
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default PropertiesFields;
diff --git a/frontend/src/widgets/ClusterConfigForm/Sections/Serdes/Serdes.tsx b/frontend/src/widgets/ClusterConfigForm/Sections/Serdes/Serdes.tsx
new file mode 100644
index 000000000..a6cd7b465
--- /dev/null
+++ b/frontend/src/widgets/ClusterConfigForm/Sections/Serdes/Serdes.tsx
@@ -0,0 +1,117 @@
+import * as React from 'react';
+import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
+import { Button } from 'components/common/Button/Button';
+import Input from 'components/common/Input/Input';
+import { useFieldArray, useFormContext } from 'react-hook-form';
+import PlusIcon from 'components/common/Icons/PlusIcon';
+import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
+import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';
+import {
+ FlexGrow1,
+ FlexRow,
+} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
+import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';
+
+import PropertiesFields from './PropertiesFields';
+
+const Serdes = () => {
+ const { control } = useFormContext();
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: 'serde',
+ });
+
+ const handleAppend = () =>
+ append({
+ name: '',
+ className: '',
+ filePath: '',
+ topicKeysPattern: '%s-key',
+ topicValuesPattern: '%s-value',
+ });
+ const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove());
+
+ const hasFields = fields.length > 0;
+
+ return (
+ <>
+
+ {hasFields && (
+
+ {fields.map((item, index) => (
+
+ ))}
+
+
+ )}
+ >
+ );
+};
+export default Serdes;
diff --git a/frontend/src/widgets/ClusterConfigForm/index.tsx b/frontend/src/widgets/ClusterConfigForm/index.tsx
index f2323c8f0..417a1100d 100644
--- a/frontend/src/widgets/ClusterConfigForm/index.tsx
+++ b/frontend/src/widgets/ClusterConfigForm/index.tsx
@@ -17,6 +17,7 @@ import { useNavigate } from 'react-router-dom';
import useBoolean from 'lib/hooks/useBoolean';
import KafkaCluster from 'widgets/ClusterConfigForm/Sections/KafkaCluster';
import SchemaRegistry from 'widgets/ClusterConfigForm/Sections/SchemaRegistry';
+import Serdes from 'widgets/ClusterConfigForm/Sections/Serdes/Serdes';
import KafkaConnect from 'widgets/ClusterConfigForm/Sections/KafkaConnect';
import Metrics from 'widgets/ClusterConfigForm/Sections/Metrics';
import CustomAuthentication from 'widgets/ClusterConfigForm/Sections/CustomAuthentication';
@@ -140,6 +141,8 @@ const ClusterConfigForm: React.FC = ({
+
+
diff --git a/frontend/src/widgets/ClusterConfigForm/schema.ts b/frontend/src/widgets/ClusterConfigForm/schema.ts
index 5c9aaa2b1..cc32e6c12 100644
--- a/frontend/src/widgets/ClusterConfigForm/schema.ts
+++ b/frontend/src/widgets/ClusterConfigForm/schema.ts
@@ -45,6 +45,27 @@ const urlWithAuthSchema = lazy((value) => {
return mixed().optional();
});
+const serdeSchema = object({
+ name: requiredString,
+ className: requiredString,
+ filePath: requiredString,
+ topicKeysPattern: requiredString,
+ topicValuesPattern: requiredString,
+ properties: array().of(
+ object({
+ key: requiredString,
+ value: requiredString,
+ })
+ ),
+});
+
+const serdesSchema = lazy((value) => {
+ if (Array.isArray(value)) {
+ return array().of(serdeSchema);
+ }
+ return mixed().optional();
+});
+
const kafkaConnectSchema = object({
name: requiredString,
address: requiredString,
@@ -255,6 +276,7 @@ const formSchema = object({
auth: authSchema,
schemaRegistry: urlWithAuthSchema,
ksql: urlWithAuthSchema,
+ serde: serdesSchema,
kafkaConnect: kafkaConnectsSchema,
masking: maskingsSchema,
metrics: metricsSchema,
diff --git a/frontend/src/widgets/ClusterConfigForm/types.ts b/frontend/src/widgets/ClusterConfigForm/types.ts
index db36a867f..293c6eb60 100644
--- a/frontend/src/widgets/ClusterConfigForm/types.ts
+++ b/frontend/src/widgets/ClusterConfigForm/types.ts
@@ -25,6 +25,18 @@ type URLWithAuth = WithAuth &
isActive?: string;
};
+export type Serde = {
+ name?: string;
+ className?: string;
+ filePath?: string;
+ topicKeysPattern?: string;
+ topicValuesPattern?: string;
+ properties: {
+ key: string;
+ value: string;
+ }[];
+};
+
type KafkaConnect = WithAuth &
WithKeystore & {
name: string;
@@ -55,6 +67,7 @@ export type ClusterConfigFormValues = {
schemaRegistry?: URLWithAuth;
ksql?: URLWithAuth;
properties?: Record;
+ serde?: Serde[];
kafkaConnect?: KafkaConnect[];
metrics?: Metrics;
customAuth: Record;
diff --git a/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts b/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts
index 31e5a1fed..cf8e33326 100644
--- a/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts
+++ b/frontend/src/widgets/ClusterConfigForm/utils/getInitialFormData.ts
@@ -30,6 +30,12 @@ const parseCredentials = (username?: string, password?: string) => {
return { isAuth: true, username, password };
};
+const parseProperties = (properties?: { [key: string]: string }) =>
+ Object.entries(properties || {}).map(([key, value]) => ({
+ key,
+ value,
+ }));
+
export const getInitialFormData = (
payload: ApplicationConfigPropertiesKafkaClusters
) => {
@@ -44,6 +50,7 @@ export const getInitialFormData = (
ksqldbServerAuth,
ksqldbServerSsl,
masking,
+ serde,
} = payload;
const initialValues: Partial = {
@@ -82,6 +89,17 @@ export const getInitialFormData = (
};
}
+ if (serde && serde.length > 0) {
+ initialValues.serde = serde.map((c) => ({
+ name: c.name,
+ className: c.className,
+ filePath: c.filePath,
+ properties: parseProperties(c.properties),
+ topicKeysPattern: c.topicKeysPattern,
+ topicValuesPattern: c.topicValuesPattern,
+ }));
+ }
+
if (kafkaConnect && kafkaConnect.length > 0) {
initialValues.kafkaConnect = kafkaConnect.map((c) => ({
name: c.name as string,
diff --git a/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts b/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts
index 35c09aa4d..d26709547 100644
--- a/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts
+++ b/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts
@@ -1,4 +1,7 @@
-import { ClusterConfigFormValues } from 'widgets/ClusterConfigForm/types';
+import {
+ ClusterConfigFormValues,
+ Serde,
+} from 'widgets/ClusterConfigForm/types';
import { ApplicationConfigPropertiesKafkaClusters } from 'generated-sources';
import { getJaasConfig } from './getJaasConfig';
@@ -35,6 +38,15 @@ const transformCustomProps = (props: Record) => {
return config;
};
+const transformSerdeProperties = (properties: Serde['properties']) => {
+ const mappedProperties: { [key: string]: string } = {};
+
+ properties.forEach(({ key, value }) => {
+ mappedProperties[key] = value;
+ });
+ return mappedProperties;
+};
+
export const transformFormDataToPayload = (data: ClusterConfigFormValues) => {
const config: ApplicationConfigPropertiesKafkaClusters = {
name: data.name,
@@ -75,6 +87,27 @@ export const transformFormDataToPayload = (data: ClusterConfigFormValues) => {
config.ksqldbServerSsl = transformToKeystore(data.ksql.keystore);
}
+ // Serde
+ if (data.serde && data.serde.length > 0) {
+ config.serde = data.serde.map(
+ ({
+ name,
+ className,
+ filePath,
+ topicKeysPattern,
+ topicValuesPattern,
+ properties,
+ }) => ({
+ name,
+ className,
+ filePath,
+ topicKeysPattern,
+ topicValuesPattern,
+ properties: transformSerdeProperties(properties),
+ })
+ );
+ }
+
// Kafka Connect
if (data.kafkaConnect && data.kafkaConnect.length > 0) {
config.kafkaConnect = data.kafkaConnect.map(