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) => ( +
+ + + + + + + +
+ +
+ remove(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(