Skip to content
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

Wizard: Support Editing serdes #866

Merged
merged 2 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -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';
Expand Down Expand Up @@ -140,6 +141,8 @@ const ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({
<hr />
<SchemaRegistry />
<hr />
<Serdes />
<hr />
<KafkaConnect />
<hr />
<KSQL />
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -255,6 +276,7 @@ const formSchema = object({
auth: authSchema,
schemaRegistry: urlWithAuthSchema,
ksql: urlWithAuthSchema,
serde: serdesSchema,
kafkaConnect: kafkaConnectsSchema,
masking: maskingsSchema,
metrics: metricsSchema,
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
) => {
Expand All @@ -44,6 +50,7 @@ export const getInitialFormData = (
ksqldbServerAuth,
ksqldbServerSsl,
masking,
serde,
} = payload;

const initialValues: Partial<ClusterConfigFormValues> = {
Expand Down Expand Up @@ -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,
Expand Down
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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Loading