Skip to content

Wizard: Support Editing serdes #866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 13, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
`;
Original file line number Diff line number Diff line change
@@ -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 (
<S.GroupFieldWrapper>
<Heading level={4}>Serde properties</Heading>
{fields.map((propsField, propsIndex) => (
<S.SerdeProperties key={propsField.id}>
<Input
name={`serde.${nestedId}.properties.${propsIndex}.key`}
placeholder="Key"
type="text"
withError
/>
<Input
name={`serde.${nestedId}.properties.${propsIndex}.value`}
placeholder="Value"
type="text"
withError
/>
<S.SerdePropertiesActions
aria-label="deleteProperty"
onClick={() => remove(propsIndex)}
>
<CloseCircleIcon aria-hidden />
</S.SerdePropertiesActions>
</S.SerdeProperties>
))}
<div>
<Button
type="button"
buttonSize="M"
buttonType="secondary"
onClick={() => append({ key: '', value: '' })}
>
<PlusIcon />
Add Property
</Button>
</div>
</S.GroupFieldWrapper>
);
};

export default PropertiesFields;
117 changes: 117 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/Sections/Serdes/Serdes.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<SectionHeader
title="Serdes"
addButtonText="Configure Serdes"
adding={!hasFields}
onClick={toggleConfig}
/>
{hasFields && (
<S.GroupFieldWrapper>
{fields.map((item, index) => (
<div key={item.id}>
<FlexRow>
<FlexGrow1>
<Input
label="Name *"
name={`serde.${index}.name`}
placeholder="Name"
type="text"
hint="Serde name"
withError
/>
<Input
label="Class Name *"
name={`serde.${index}.className`}
placeholder="className"
type="text"
hint="Serde class name"
withError
/>
<Input
label="File Path *"
name={`serde.${index}.filePath`}
placeholder="serde file path"
type="text"
hint="Serde file path"
withError
/>
<Input
label="Topic Keys Pattern *"
name={`serde.${index}.topicKeysPattern`}
placeholder="topicKeysPattern"
type="text"
hint="Serde topic keys pattern"
withError
/>
<Input
label="Topic Values Pattern *"
name={`serde.${index}.topicValuesPattern`}
placeholder="topicValuesPattern"
type="text"
hint="Serde topic values pattern"
withError
/>
<hr />
<PropertiesFields nestedId={index} />
</FlexGrow1>
<S.RemoveButton onClick={() => remove(index)}>
<IconButtonWrapper aria-label="deleteProperty">
<CloseCircleIcon aria-hidden />
</IconButtonWrapper>
</S.RemoveButton>
</FlexRow>

<hr />
</div>
))}
<Button
type="button"
buttonSize="M"
buttonType="secondary"
onClick={handleAppend}
>
<PlusIcon />
Add Serde
</Button>
</S.GroupFieldWrapper>
)}
</>
);
};
export default Serdes;
3 changes: 3 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/index.tsx
Original file line number Diff line number Diff line change
@@ -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<ClusterConfigFormProps> = ({
<hr />
<SchemaRegistry />
<hr />
<Serdes />
<hr />
<KafkaConnect />
<hr />
<KSQL />
22 changes: 22 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/schema.ts
Original file line number Diff line number Diff line change
@@ -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,
13 changes: 13 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/types.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
serde?: Serde[];
kafkaConnect?: KafkaConnect[];
metrics?: Metrics;
customAuth: Record<string, string>;
Original file line number Diff line number Diff line change
@@ -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<ClusterConfigFormValues> = {
@@ -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,
Original file line number Diff line number Diff line change
@@ -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<string, string>) => {
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(