Skip to content

Commit 6b7fda4

Browse files
VixtirPavel MakarichevHaarolean
authored
FE: Add forms for an ACL creating (#188)
Co-authored-by: Pavel Makarichev <pavel.makarichev@almatech.dev> Co-authored-by: Roman Zabaluev <gpg@haarolean.dev>
1 parent 3b4fb20 commit 6b7fda4

File tree

40 files changed

+1321
-46
lines changed

40 files changed

+1321
-46
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createContext } from 'react';
2+
3+
interface ACLFormContextProps {
4+
close: () => void;
5+
}
6+
const ACLFormContext = createContext<ACLFormContextProps | null>(null);
7+
8+
export default ACLFormContext;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { FC, useContext } from 'react';
2+
import { yupResolver } from '@hookform/resolvers/yup';
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
import { useCreateCustomAcl } from 'lib/hooks/api/acl';
5+
import ControlledRadio from 'components/common/Radio/ControlledRadio';
6+
import Input from 'components/common/Input/Input';
7+
import ControlledSelect from 'components/common/Select/ControlledSelect';
8+
import { matchTypeOptions } from 'components/ACLPage/Form/constants';
9+
import useAppParams from 'lib/hooks/useAppParams';
10+
import { ClusterName } from 'redux/interfaces';
11+
import * as S from 'components/ACLPage/Form/Form.styled';
12+
import ACLFormContext from 'components/ACLPage/Form/AclFormContext';
13+
import { AclDetailedFormProps } from 'components/ACLPage/Form/types';
14+
15+
import formSchema from './schema';
16+
import { FormValues } from './types';
17+
import { toRequest } from './lib';
18+
import {
19+
defaultValues,
20+
operations,
21+
permissions,
22+
resourceTypes,
23+
} from './constants';
24+
25+
const CustomACLForm: FC<AclDetailedFormProps> = ({ formRef }) => {
26+
const context = useContext(ACLFormContext);
27+
28+
const methods = useForm<FormValues>({
29+
mode: 'all',
30+
resolver: yupResolver(formSchema),
31+
defaultValues: { ...defaultValues },
32+
});
33+
34+
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
35+
const create = useCreateCustomAcl(clusterName);
36+
37+
const onSubmit = async (data: FormValues) => {
38+
try {
39+
const resource = toRequest(data);
40+
await create.createResource(resource);
41+
context?.close();
42+
} catch (e) {
43+
// no custom error
44+
}
45+
};
46+
47+
return (
48+
<FormProvider {...methods}>
49+
<S.Form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}>
50+
<hr />
51+
<S.Field>
52+
<S.Label htmlFor="principal">Principal</S.Label>
53+
<Input
54+
name="principal"
55+
id="principal"
56+
placeholder="Principal"
57+
withError
58+
/>
59+
</S.Field>
60+
61+
<S.Field>
62+
<S.Label htmlFor="host">Host restriction</S.Label>
63+
<Input name="host" id="host" placeholder="Host" withError />
64+
</S.Field>
65+
<hr />
66+
67+
<S.Field>
68+
<S.Label htmlFor="resourceType">Resource type</S.Label>
69+
<ControlledSelect options={resourceTypes} name="resourceType" />
70+
</S.Field>
71+
72+
<S.Field>
73+
<S.Label>Operations</S.Label>
74+
<S.ControlList>
75+
<ControlledRadio name="permission" options={permissions} />
76+
<ControlledSelect options={operations} name="operation" />
77+
</S.ControlList>
78+
</S.Field>
79+
80+
<S.Field>
81+
<S.Field>Matching pattern</S.Field>
82+
<S.ControlList>
83+
<ControlledRadio
84+
name="namePatternType"
85+
options={matchTypeOptions}
86+
/>
87+
<Input
88+
name="resourceName"
89+
id="resourceName"
90+
placeholder="Matching pattern"
91+
withError
92+
/>
93+
</S.ControlList>
94+
</S.Field>
95+
</S.Form>
96+
</FormProvider>
97+
);
98+
};
99+
100+
export default React.memo(CustomACLForm);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { SelectOption } from 'components/common/Select/Select';
2+
import {
3+
KafkaAclOperationEnum,
4+
KafkaAclPermissionEnum,
5+
KafkaAclResourceType,
6+
} from 'generated-sources';
7+
import { RadioOption } from 'components/common/Radio/types';
8+
9+
import { FormValues } from './types';
10+
11+
function toOptionsArray<T extends string>(
12+
list: T[],
13+
unknown: T
14+
): SelectOption<T>[] {
15+
return list.reduce<SelectOption<T>[]>((acc, cur) => {
16+
if (cur !== unknown) {
17+
acc.push({ label: cur, value: cur });
18+
}
19+
20+
return acc;
21+
}, []);
22+
}
23+
24+
export const resourceTypes = toOptionsArray(
25+
Object.values(KafkaAclResourceType),
26+
KafkaAclResourceType.UNKNOWN
27+
);
28+
29+
export const operations = toOptionsArray(
30+
Object.values(KafkaAclOperationEnum),
31+
KafkaAclOperationEnum.UNKNOWN
32+
);
33+
34+
export const permissions: RadioOption[] = [
35+
{
36+
value: KafkaAclPermissionEnum.ALLOW,
37+
itemType: 'green',
38+
},
39+
{
40+
value: KafkaAclPermissionEnum.DENY,
41+
itemType: 'red',
42+
},
43+
];
44+
45+
export const defaultValues: Partial<FormValues> = {
46+
resourceType: resourceTypes[0].value as KafkaAclResourceType,
47+
operation: operations[0].value as KafkaAclOperationEnum,
48+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { KafkaAcl, KafkaAclNamePatternType } from 'generated-sources';
2+
import isRegex from 'lib/isRegex';
3+
import { MatchType } from 'components/ACLPage/Form/types';
4+
5+
import { FormValues } from './types';
6+
7+
export function toRequest(formValue: FormValues): KafkaAcl {
8+
let namePatternType: KafkaAclNamePatternType;
9+
if (formValue.namePatternType === MatchType.PREFIXED) {
10+
namePatternType = KafkaAclNamePatternType.PREFIXED;
11+
} else if (isRegex(formValue.resourceName)) {
12+
namePatternType = KafkaAclNamePatternType.MATCH;
13+
} else {
14+
namePatternType = KafkaAclNamePatternType.LITERAL;
15+
}
16+
17+
return {
18+
resourceType: formValue.resourceType,
19+
resourceName: formValue.resourceName,
20+
namePatternType,
21+
principal: formValue.principal,
22+
host: formValue.host,
23+
operation: formValue.operation,
24+
permission: formValue.permission,
25+
};
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { object, string } from 'yup';
2+
3+
const formSchema = object({
4+
resourceType: string().required(),
5+
resourceName: string().required(),
6+
namePatternType: string().required(),
7+
principal: string().required(),
8+
host: string().required(),
9+
operation: string().required(),
10+
permission: string().required(),
11+
});
12+
13+
export default formSchema;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {
2+
KafkaAclOperationEnum,
3+
KafkaAclPermissionEnum,
4+
KafkaAclResourceType,
5+
} from 'generated-sources';
6+
import { MatchType } from 'components/ACLPage/Form/types';
7+
8+
export interface FormValues {
9+
resourceType: KafkaAclResourceType;
10+
resourceName: string;
11+
namePatternType: MatchType;
12+
principal: string;
13+
host: string;
14+
operation: KafkaAclOperationEnum;
15+
permission: KafkaAclPermissionEnum;
16+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, { FC, useContext } from 'react';
2+
import { yupResolver } from '@hookform/resolvers/yup';
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
import { ClusterName } from 'redux/interfaces';
5+
import { useCreateConsumersAcl } from 'lib/hooks/api/acl';
6+
import useAppParams from 'lib/hooks/useAppParams';
7+
import ControlledMultiSelect from 'components/common/MultiSelect/ControlledMultiSelect';
8+
import Input from 'components/common/Input/Input';
9+
import * as S from 'components/ACLPage/Form/Form.styled';
10+
import { AclDetailedFormProps, MatchType } from 'components/ACLPage/Form/types';
11+
import useTopicsOptions from 'components/ACLPage/lib/useTopicsOptions';
12+
import useConsumerGroupsOptions from 'components/ACLPage/lib/useConsumerGroupsOptions';
13+
import ACLFormContext from 'components/ACLPage/Form/AclFormContext';
14+
import MatchTypeSelector from 'components/ACLPage/Form/components/MatchTypeSelector';
15+
16+
import formSchema from './schema';
17+
import { toRequest } from './lib';
18+
import { FormValues } from './types';
19+
20+
const ForConsumersForm: FC<AclDetailedFormProps> = ({ formRef }) => {
21+
const context = useContext(ACLFormContext);
22+
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
23+
const create = useCreateConsumersAcl(clusterName);
24+
const methods = useForm<FormValues>({
25+
mode: 'all',
26+
resolver: yupResolver(formSchema),
27+
});
28+
29+
const { setValue } = methods;
30+
31+
const onSubmit = async (data: FormValues) => {
32+
try {
33+
await create.createResource(toRequest(data));
34+
context?.close();
35+
} catch (e) {
36+
// no custom error
37+
}
38+
};
39+
40+
const topics = useTopicsOptions(clusterName);
41+
const consumerGroups = useConsumerGroupsOptions(clusterName);
42+
43+
const onTopicTypeChange = (value: string) => {
44+
if (value === MatchType.EXACT) {
45+
setValue('topicsPrefix', undefined);
46+
} else {
47+
setValue('topics', undefined);
48+
}
49+
};
50+
51+
const onConsumerGroupTypeChange = (value: string) => {
52+
if (value === MatchType.EXACT) {
53+
setValue('consumerGroupsPrefix', undefined);
54+
} else {
55+
setValue('consumerGroups', undefined);
56+
}
57+
};
58+
59+
return (
60+
<FormProvider {...methods}>
61+
<S.Form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}>
62+
<hr />
63+
<S.Field>
64+
<S.Label htmlFor="principal">Principal</S.Label>
65+
<Input
66+
name="principal"
67+
id="principal"
68+
placeholder="Principal"
69+
withError
70+
/>
71+
</S.Field>
72+
73+
<S.Field>
74+
<S.Label htmlFor="host">Host restriction</S.Label>
75+
<Input name="host" id="host" placeholder="Host" withError />
76+
</S.Field>
77+
<hr />
78+
79+
<S.Field>
80+
<S.Label>From Topic(s)</S.Label>
81+
<S.ControlList>
82+
<MatchTypeSelector
83+
exact={<ControlledMultiSelect name="topics" options={topics} />}
84+
prefixed={<Input name="topicsPrefix" placeholder="Prefix..." />}
85+
onChange={onTopicTypeChange}
86+
/>
87+
</S.ControlList>
88+
</S.Field>
89+
90+
<S.Field>
91+
<S.Field>Consumer group(s)</S.Field>
92+
<S.ControlList>
93+
<MatchTypeSelector
94+
exact={
95+
<ControlledMultiSelect
96+
name="consumerGroups"
97+
options={consumerGroups}
98+
/>
99+
}
100+
prefixed={
101+
<Input name="consumerGroupsPrefix" placeholder="Prefix..." />
102+
}
103+
onChange={onConsumerGroupTypeChange}
104+
/>
105+
</S.ControlList>
106+
</S.Field>
107+
</S.Form>
108+
</FormProvider>
109+
);
110+
};
111+
112+
export default React.memo(ForConsumersForm);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { CreateConsumerAcl } from 'generated-sources/models/CreateConsumerAcl';
2+
3+
import { FormValues } from './types';
4+
5+
export const toRequest = (formValues: FormValues): CreateConsumerAcl => {
6+
return {
7+
principal: formValues.principal,
8+
host: formValues.host,
9+
consumerGroups: formValues.consumerGroups?.map((opt) => opt.value),
10+
consumerGroupsPrefix: formValues.consumerGroupsPrefix,
11+
topics: formValues.topics?.map((opt) => opt.value),
12+
topicsPrefix: formValues.topicsPrefix,
13+
};
14+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { array, object, string } from 'yup';
2+
3+
const formSchema = object({
4+
principal: string().required(),
5+
host: string().required(),
6+
topics: array().of(
7+
object().shape({
8+
label: string().required(),
9+
value: string().required(),
10+
})
11+
),
12+
topicsPrefix: string(),
13+
consumerGroups: array().of(
14+
object().shape({
15+
label: string().required(),
16+
value: string().required(),
17+
})
18+
),
19+
consumerGroupsPrefix: string(),
20+
});
21+
22+
export default formSchema;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Option } from 'react-multi-select-component';
2+
3+
export interface FormValues {
4+
principal: string;
5+
host: string;
6+
topics?: Option[];
7+
topicsPrefix?: string;
8+
consumerGroups?: Option[];
9+
consumerGroupsPrefix?: string;
10+
}

0 commit comments

Comments
 (0)