Skip to content

FE: Wizard: Support Editing masking #873

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 4 commits into from
Mar 9, 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
20 changes: 19 additions & 1 deletion frontend/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ConfigurationParameters, ConsumerGroupState } from 'generated-sources';
import {
ApplicationConfigPropertiesKafkaMaskingTypeEnum,
ConfigurationParameters,
ConsumerGroupState,
} from 'generated-sources';

declare global {
interface Window {
Expand Down Expand Up @@ -101,6 +105,20 @@ export const METRICS_OPTIONS = [
{ value: 'JMX', label: 'JMX' },
{ value: 'PROMETHEUS', label: 'PROMETHEUS' },
];
export const MASKING_OPTIONS = [
{
value: ApplicationConfigPropertiesKafkaMaskingTypeEnum.MASK,
label: 'MASK',
},
{
value: ApplicationConfigPropertiesKafkaMaskingTypeEnum.REMOVE,
label: 'REMOVE',
},
{
value: ApplicationConfigPropertiesKafkaMaskingTypeEnum.REPLACE,
label: 'REPLACE',
},
];

export const CONSUMER_GROUP_STATE_TOOLTIPS: Record<ConsumerGroupState, string> =
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const FlexGrow1 = styled.div`
flex-direction: column;
display: flex;
`;

// KafkaCluster
export const BootstrapServer = styled(InputContainer)`
grid-template-columns: 3fr 110px 30px;
Expand All @@ -58,5 +59,23 @@ export const FileUploadInputWrapper = styled.div`
display: flex;
height: 40px;
align-items: center;
color: ${({ theme }) => theme.clusterConfigForm.fileInput.color}};
color: ${({ theme }) => theme.clusterConfigForm.fileInput.color};
`;

// Masking
export const FieldWrapper = styled.div`
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
`;
export const FieldContainer = styled.div`
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
`;
export const Error = styled.p`
color: ${({ theme }) => theme.input.error};
font-size: 12px;
`;
225 changes: 225 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/Sections/Masking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
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 {
FieldContainer,
FieldWrapper,
FlexGrow1,
FlexRow,
} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';
import { MASKING_OPTIONS } from 'lib/constants';
import ControlledSelect from 'components/common/Select/ControlledSelect';
import { FormError } from 'components/common/Input/Input.styled';
import { ErrorMessage } from '@hookform/error-message';

const Fields = ({ nestedIdx }: { nestedIdx: number }) => {
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: `masking.${nestedIdx}.fields`,
});

const handleAppend = () => append({ value: '' });

return (
<FlexGrow1>
<FieldWrapper>
<FieldWrapper>
{fields.map((item, index) => (
<FieldContainer key={item.id}>
<Input
label="Field"
name={`masking.${nestedIdx}.fields.${index}.value`}
placeholder="Field"
type="text"
withError
/>

{fields.length > 1 && (
<S.RemoveButton
style={{ marginTop: '18px' }}
onClick={() => remove(index)}
>
<IconButtonWrapper aria-label="deleteProperty">
<CloseCircleIcon aria-hidden />
</IconButtonWrapper>
</S.RemoveButton>
)}
</FieldContainer>
))}
</FieldWrapper>

<Button
style={{ marginTop: '20px' }}
type="button"
buttonSize="M"
buttonType="secondary"
onClick={handleAppend}
>
<PlusIcon />
Add Field
</Button>
</FieldWrapper>

<FormError>
<ErrorMessage name={`masking.${nestedIdx}.fields`} />
</FormError>
</FlexGrow1>
);
};

const MaskingCharReplacement = ({ nestedIdx }: { nestedIdx: number }) => {
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: `masking.${nestedIdx}.maskingCharsReplacement`,
});

const handleAppend = () => append({ value: '' });

return (
<FlexGrow1>
<FieldWrapper>
<FieldWrapper>
{fields.map((item, index) => (
<FieldContainer key={item.id}>
<Input
label="Field"
name={`masking.${nestedIdx}.maskingCharsReplacement.${index}.value`}
placeholder="Field"
type="text"
withError
/>

{fields.length > 1 && (
<S.RemoveButton
style={{ marginTop: '18px' }}
onClick={() => remove(index)}
>
<IconButtonWrapper aria-label="deleteProperty">
<CloseCircleIcon aria-hidden />
</IconButtonWrapper>
</S.RemoveButton>
)}
</FieldContainer>
))}
</FieldWrapper>

<Button
style={{ marginTop: '20px' }}
type="button"
buttonSize="M"
buttonType="secondary"
onClick={handleAppend}
>
<PlusIcon />
Add Masking Chars Replacement
</Button>
</FieldWrapper>

<FormError>
<ErrorMessage name={`masking.${nestedIdx}.maskingCharsReplacement`} />
</FormError>
</FlexGrow1>
);
};

const Masking = () => {
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({
control,
name: 'masking',
});
const handleAppend = () =>
append({
type: undefined,
fields: [{ value: '' }],
fieldsNamePattern: '',
maskingCharsReplacement: [{ value: '' }],
replacement: '',
topicKeysPattern: '',
topicValuesPattern: '',
});
const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove());

const hasFields = fields.length > 0;

return (
<>
<SectionHeader
title="Masking"
addButtonText="Configure Masking"
adding={!hasFields}
onClick={toggleConfig}
/>
{hasFields && (
<S.GroupFieldWrapper>
{fields.map((item, index) => (
<div key={item.id}>
<FlexRow>
<FlexGrow1>
<ControlledSelect
name={`masking.${index}.type`}
label="Masking Type *"
placeholder="Choose masking type"
options={MASKING_OPTIONS}
/>
<Fields nestedIdx={index} />
<Input
label="Fields name pattern"
name={`masking.${index}.fieldsNamePattern`}
placeholder="Pattern"
type="text"
withError
/>
<MaskingCharReplacement nestedIdx={index} />
<Input
label="Replacement"
name={`masking.${index}.replacement`}
placeholder="Replacement"
type="text"
/>
<Input
label="Topic Keys Pattern"
name={`masking.${index}.topicKeysPattern`}
placeholder="Keys pattern"
type="text"
/>
<Input
label="Topic Values Pattern"
name={`masking.${index}.topicValuesPattern`}
placeholder="Values pattern"
type="text"
/>
</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 Masking
</Button>
</S.GroupFieldWrapper>
)}
</>
);
};
export default Masking;
3 changes: 3 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Metrics from 'widgets/ClusterConfigForm/Sections/Metrics';
import CustomAuthentication from 'widgets/ClusterConfigForm/Sections/CustomAuthentication';
import Authentication from 'widgets/ClusterConfigForm/Sections/Authentication/Authentication';
import KSQL from 'widgets/ClusterConfigForm/Sections/KSQL';
import Masking from 'widgets/ClusterConfigForm/Sections/Masking';
import { useConfirm } from 'lib/hooks/useConfirm';

interface ClusterConfigFormProps {
Expand Down Expand Up @@ -145,6 +146,8 @@ const ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({
<hr />
<Metrics />
<hr />
<Masking />
<hr />
<S.ButtonWrapper>
<Button
buttonSize="L"
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/widgets/ClusterConfigForm/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { object, string, number, array, boolean, mixed, lazy } from 'yup';
import { ApplicationConfigPropertiesKafkaMaskingTypeEnum } from 'generated-sources';

const requiredString = string().required('required field');

Expand Down Expand Up @@ -179,6 +180,71 @@ const authSchema = lazy((value) => {
return mixed().optional();
});

const maskingSchema = object({
type: mixed<ApplicationConfigPropertiesKafkaMaskingTypeEnum>()
.oneOf(Object.values(ApplicationConfigPropertiesKafkaMaskingTypeEnum))
.required('required field'),
fields: array().of(
object().shape({
value: string().test(
'fieldsOrPattern',
'Either fields or fieldsNamePattern is required',
(value, { path, parent, ...ctx }) => {
const maskingItem = ctx.from?.[1].value;

if (value && value.trim() !== '') {
return true;
}

const otherFieldHasValue =
maskingItem.fields &&
maskingItem.fields.some(
(field: { value: string }) =>
field.value && field.value.trim() !== ''
);

if (otherFieldHasValue) {
return true;
}

const hasPattern =
maskingItem.fieldsNamePattern &&
maskingItem.fieldsNamePattern.trim() !== '';

return hasPattern;
}
),
})
),
fieldsNamePattern: string().test(
'fieldsOrPattern',
'Either fields or fieldsNamePattern is required',
(value, { parent }) => {
const hasValidFields =
parent.fields &&
parent.fields.length > 0 &&
parent.fields.some(
(field: { value: string }) => field.value && field.value.trim() !== ''
);

const hasPattern = value && value.trim() !== '';

return hasValidFields || hasPattern;
}
),
maskingCharsReplacement: array().of(object().shape({ value: string() })),
replacement: string(),
topicKeysPattern: string(),
topicValuesPattern: string(),
});

const maskingsSchema = lazy((value) => {
if (Array.isArray(value)) {
return array().of(maskingSchema);
}
return mixed().optional();
});

const formSchema = object({
name: string()
.required('required field')
Expand All @@ -190,6 +256,7 @@ const formSchema = object({
schemaRegistry: urlWithAuthSchema,
ksql: urlWithAuthSchema,
kafkaConnect: kafkaConnectsSchema,
masking: maskingsSchema,
metrics: metricsSchema,
});

Expand Down
Loading
Loading