Skip to content

Commit 74f7c08

Browse files
Leshe4kaLeshe4kaHaarolean
authored
FE: Wizard: Support Editing masking (#873)
Co-authored-by: Leshe4ka <[email protected]> Co-authored-by: Roman Zabaluev <[email protected]>
1 parent 1f9cbbb commit 74f7c08

File tree

8 files changed

+371
-2
lines changed

8 files changed

+371
-2
lines changed

frontend/src/lib/constants.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { ConfigurationParameters, ConsumerGroupState } from 'generated-sources';
1+
import {
2+
ApplicationConfigPropertiesKafkaMaskingTypeEnum,
3+
ConfigurationParameters,
4+
ConsumerGroupState,
5+
} from 'generated-sources';
26

37
declare global {
48
interface Window {
@@ -101,6 +105,20 @@ export const METRICS_OPTIONS = [
101105
{ value: 'JMX', label: 'JMX' },
102106
{ value: 'PROMETHEUS', label: 'PROMETHEUS' },
103107
];
108+
export const MASKING_OPTIONS = [
109+
{
110+
value: ApplicationConfigPropertiesKafkaMaskingTypeEnum.MASK,
111+
label: 'MASK',
112+
},
113+
{
114+
value: ApplicationConfigPropertiesKafkaMaskingTypeEnum.REMOVE,
115+
label: 'REMOVE',
116+
},
117+
{
118+
value: ApplicationConfigPropertiesKafkaMaskingTypeEnum.REPLACE,
119+
label: 'REPLACE',
120+
},
121+
];
104122

105123
export const CONSUMER_GROUP_STATE_TOOLTIPS: Record<ConsumerGroupState, string> =
106124
{

frontend/src/widgets/ClusterConfigForm/ClusterConfigForm.styled.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const FlexGrow1 = styled.div`
4141
flex-direction: column;
4242
display: flex;
4343
`;
44+
4445
// KafkaCluster
4546
export const BootstrapServer = styled(InputContainer)`
4647
grid-template-columns: 3fr 110px 30px;
@@ -58,5 +59,23 @@ export const FileUploadInputWrapper = styled.div`
5859
display: flex;
5960
height: 40px;
6061
align-items: center;
61-
color: ${({ theme }) => theme.clusterConfigForm.fileInput.color}};
62+
color: ${({ theme }) => theme.clusterConfigForm.fileInput.color};
63+
`;
64+
65+
// Masking
66+
export const FieldWrapper = styled.div`
67+
display: flex;
68+
gap: 8px;
69+
align-items: center;
70+
flex-wrap: wrap;
71+
`;
72+
export const FieldContainer = styled.div`
73+
display: flex;
74+
flex-direction: row;
75+
gap: 8px;
76+
align-items: center;
77+
`;
78+
export const Error = styled.p`
79+
color: ${({ theme }) => theme.input.error};
80+
font-size: 12px;
6281
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import * as React from 'react';
2+
import * as S from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
3+
import { Button } from 'components/common/Button/Button';
4+
import Input from 'components/common/Input/Input';
5+
import { useFieldArray, useFormContext } from 'react-hook-form';
6+
import PlusIcon from 'components/common/Icons/PlusIcon';
7+
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
8+
import CloseCircleIcon from 'components/common/Icons/CloseCircleIcon';
9+
import {
10+
FieldContainer,
11+
FieldWrapper,
12+
FlexGrow1,
13+
FlexRow,
14+
} from 'widgets/ClusterConfigForm/ClusterConfigForm.styled';
15+
import SectionHeader from 'widgets/ClusterConfigForm/common/SectionHeader';
16+
import { MASKING_OPTIONS } from 'lib/constants';
17+
import ControlledSelect from 'components/common/Select/ControlledSelect';
18+
import { FormError } from 'components/common/Input/Input.styled';
19+
import { ErrorMessage } from '@hookform/error-message';
20+
21+
const Fields = ({ nestedIdx }: { nestedIdx: number }) => {
22+
const { control } = useFormContext();
23+
const { fields, append, remove } = useFieldArray({
24+
control,
25+
name: `masking.${nestedIdx}.fields`,
26+
});
27+
28+
const handleAppend = () => append({ value: '' });
29+
30+
return (
31+
<FlexGrow1>
32+
<FieldWrapper>
33+
<FieldWrapper>
34+
{fields.map((item, index) => (
35+
<FieldContainer key={item.id}>
36+
<Input
37+
label="Field"
38+
name={`masking.${nestedIdx}.fields.${index}.value`}
39+
placeholder="Field"
40+
type="text"
41+
withError
42+
/>
43+
44+
{fields.length > 1 && (
45+
<S.RemoveButton
46+
style={{ marginTop: '18px' }}
47+
onClick={() => remove(index)}
48+
>
49+
<IconButtonWrapper aria-label="deleteProperty">
50+
<CloseCircleIcon aria-hidden />
51+
</IconButtonWrapper>
52+
</S.RemoveButton>
53+
)}
54+
</FieldContainer>
55+
))}
56+
</FieldWrapper>
57+
58+
<Button
59+
style={{ marginTop: '20px' }}
60+
type="button"
61+
buttonSize="M"
62+
buttonType="secondary"
63+
onClick={handleAppend}
64+
>
65+
<PlusIcon />
66+
Add Field
67+
</Button>
68+
</FieldWrapper>
69+
70+
<FormError>
71+
<ErrorMessage name={`masking.${nestedIdx}.fields`} />
72+
</FormError>
73+
</FlexGrow1>
74+
);
75+
};
76+
77+
const MaskingCharReplacement = ({ nestedIdx }: { nestedIdx: number }) => {
78+
const { control } = useFormContext();
79+
const { fields, append, remove } = useFieldArray({
80+
control,
81+
name: `masking.${nestedIdx}.maskingCharsReplacement`,
82+
});
83+
84+
const handleAppend = () => append({ value: '' });
85+
86+
return (
87+
<FlexGrow1>
88+
<FieldWrapper>
89+
<FieldWrapper>
90+
{fields.map((item, index) => (
91+
<FieldContainer key={item.id}>
92+
<Input
93+
label="Field"
94+
name={`masking.${nestedIdx}.maskingCharsReplacement.${index}.value`}
95+
placeholder="Field"
96+
type="text"
97+
withError
98+
/>
99+
100+
{fields.length > 1 && (
101+
<S.RemoveButton
102+
style={{ marginTop: '18px' }}
103+
onClick={() => remove(index)}
104+
>
105+
<IconButtonWrapper aria-label="deleteProperty">
106+
<CloseCircleIcon aria-hidden />
107+
</IconButtonWrapper>
108+
</S.RemoveButton>
109+
)}
110+
</FieldContainer>
111+
))}
112+
</FieldWrapper>
113+
114+
<Button
115+
style={{ marginTop: '20px' }}
116+
type="button"
117+
buttonSize="M"
118+
buttonType="secondary"
119+
onClick={handleAppend}
120+
>
121+
<PlusIcon />
122+
Add Masking Chars Replacement
123+
</Button>
124+
</FieldWrapper>
125+
126+
<FormError>
127+
<ErrorMessage name={`masking.${nestedIdx}.maskingCharsReplacement`} />
128+
</FormError>
129+
</FlexGrow1>
130+
);
131+
};
132+
133+
const Masking = () => {
134+
const { control } = useFormContext();
135+
const { fields, append, remove } = useFieldArray({
136+
control,
137+
name: 'masking',
138+
});
139+
const handleAppend = () =>
140+
append({
141+
type: undefined,
142+
fields: [{ value: '' }],
143+
fieldsNamePattern: '',
144+
maskingCharsReplacement: [{ value: '' }],
145+
replacement: '',
146+
topicKeysPattern: '',
147+
topicValuesPattern: '',
148+
});
149+
const toggleConfig = () => (fields.length === 0 ? handleAppend() : remove());
150+
151+
const hasFields = fields.length > 0;
152+
153+
return (
154+
<>
155+
<SectionHeader
156+
title="Masking"
157+
addButtonText="Configure Masking"
158+
adding={!hasFields}
159+
onClick={toggleConfig}
160+
/>
161+
{hasFields && (
162+
<S.GroupFieldWrapper>
163+
{fields.map((item, index) => (
164+
<div key={item.id}>
165+
<FlexRow>
166+
<FlexGrow1>
167+
<ControlledSelect
168+
name={`masking.${index}.type`}
169+
label="Masking Type *"
170+
placeholder="Choose masking type"
171+
options={MASKING_OPTIONS}
172+
/>
173+
<Fields nestedIdx={index} />
174+
<Input
175+
label="Fields name pattern"
176+
name={`masking.${index}.fieldsNamePattern`}
177+
placeholder="Pattern"
178+
type="text"
179+
withError
180+
/>
181+
<MaskingCharReplacement nestedIdx={index} />
182+
<Input
183+
label="Replacement"
184+
name={`masking.${index}.replacement`}
185+
placeholder="Replacement"
186+
type="text"
187+
/>
188+
<Input
189+
label="Topic Keys Pattern"
190+
name={`masking.${index}.topicKeysPattern`}
191+
placeholder="Keys pattern"
192+
type="text"
193+
/>
194+
<Input
195+
label="Topic Values Pattern"
196+
name={`masking.${index}.topicValuesPattern`}
197+
placeholder="Values pattern"
198+
type="text"
199+
/>
200+
</FlexGrow1>
201+
<S.RemoveButton onClick={() => remove(index)}>
202+
<IconButtonWrapper aria-label="deleteProperty">
203+
<CloseCircleIcon aria-hidden />
204+
</IconButtonWrapper>
205+
</S.RemoveButton>
206+
</FlexRow>
207+
208+
<hr />
209+
</div>
210+
))}
211+
<Button
212+
type="button"
213+
buttonSize="M"
214+
buttonType="secondary"
215+
onClick={handleAppend}
216+
>
217+
<PlusIcon />
218+
Add Masking
219+
</Button>
220+
</S.GroupFieldWrapper>
221+
)}
222+
</>
223+
);
224+
};
225+
export default Masking;

frontend/src/widgets/ClusterConfigForm/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Metrics from 'widgets/ClusterConfigForm/Sections/Metrics';
2222
import CustomAuthentication from 'widgets/ClusterConfigForm/Sections/CustomAuthentication';
2323
import Authentication from 'widgets/ClusterConfigForm/Sections/Authentication/Authentication';
2424
import KSQL from 'widgets/ClusterConfigForm/Sections/KSQL';
25+
import Masking from 'widgets/ClusterConfigForm/Sections/Masking';
2526
import { useConfirm } from 'lib/hooks/useConfirm';
2627

2728
interface ClusterConfigFormProps {
@@ -145,6 +146,8 @@ const ClusterConfigForm: React.FC<ClusterConfigFormProps> = ({
145146
<hr />
146147
<Metrics />
147148
<hr />
149+
<Masking />
150+
<hr />
148151
<S.ButtonWrapper>
149152
<Button
150153
buttonSize="L"

frontend/src/widgets/ClusterConfigForm/schema.ts

+67
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { object, string, number, array, boolean, mixed, lazy } from 'yup';
2+
import { ApplicationConfigPropertiesKafkaMaskingTypeEnum } from 'generated-sources';
23

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

@@ -179,6 +180,71 @@ const authSchema = lazy((value) => {
179180
return mixed().optional();
180181
});
181182

183+
const maskingSchema = object({
184+
type: mixed<ApplicationConfigPropertiesKafkaMaskingTypeEnum>()
185+
.oneOf(Object.values(ApplicationConfigPropertiesKafkaMaskingTypeEnum))
186+
.required('required field'),
187+
fields: array().of(
188+
object().shape({
189+
value: string().test(
190+
'fieldsOrPattern',
191+
'Either fields or fieldsNamePattern is required',
192+
(value, { path, parent, ...ctx }) => {
193+
const maskingItem = ctx.from?.[1].value;
194+
195+
if (value && value.trim() !== '') {
196+
return true;
197+
}
198+
199+
const otherFieldHasValue =
200+
maskingItem.fields &&
201+
maskingItem.fields.some(
202+
(field: { value: string }) =>
203+
field.value && field.value.trim() !== ''
204+
);
205+
206+
if (otherFieldHasValue) {
207+
return true;
208+
}
209+
210+
const hasPattern =
211+
maskingItem.fieldsNamePattern &&
212+
maskingItem.fieldsNamePattern.trim() !== '';
213+
214+
return hasPattern;
215+
}
216+
),
217+
})
218+
),
219+
fieldsNamePattern: string().test(
220+
'fieldsOrPattern',
221+
'Either fields or fieldsNamePattern is required',
222+
(value, { parent }) => {
223+
const hasValidFields =
224+
parent.fields &&
225+
parent.fields.length > 0 &&
226+
parent.fields.some(
227+
(field: { value: string }) => field.value && field.value.trim() !== ''
228+
);
229+
230+
const hasPattern = value && value.trim() !== '';
231+
232+
return hasValidFields || hasPattern;
233+
}
234+
),
235+
maskingCharsReplacement: array().of(object().shape({ value: string() })),
236+
replacement: string(),
237+
topicKeysPattern: string(),
238+
topicValuesPattern: string(),
239+
});
240+
241+
const maskingsSchema = lazy((value) => {
242+
if (Array.isArray(value)) {
243+
return array().of(maskingSchema);
244+
}
245+
return mixed().optional();
246+
});
247+
182248
const formSchema = object({
183249
name: string()
184250
.required('required field')
@@ -190,6 +256,7 @@ const formSchema = object({
190256
schemaRegistry: urlWithAuthSchema,
191257
ksql: urlWithAuthSchema,
192258
kafkaConnect: kafkaConnectsSchema,
259+
masking: maskingsSchema,
193260
metrics: metricsSchema,
194261
});
195262

0 commit comments

Comments
 (0)