Skip to content

Commit

Permalink
FE: Add forms for an ACL creating (#188)
Browse files Browse the repository at this point in the history
Co-authored-by: Pavel Makarichev <[email protected]>
Co-authored-by: Roman Zabaluev <[email protected]>
  • Loading branch information
3 people authored Mar 18, 2024
1 parent 3b4fb20 commit 6b7fda4
Show file tree
Hide file tree
Showing 40 changed files with 1,321 additions and 46 deletions.
8 changes: 8 additions & 0 deletions frontend/src/components/ACLPage/Form/AclFormContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from 'react';

interface ACLFormContextProps {
close: () => void;
}
const ACLFormContext = createContext<ACLFormContextProps | null>(null);

export default ACLFormContext;
100 changes: 100 additions & 0 deletions frontend/src/components/ACLPage/Form/CustomACL/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { FC, useContext } from 'react';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { useCreateCustomAcl } from 'lib/hooks/api/acl';
import ControlledRadio from 'components/common/Radio/ControlledRadio';
import Input from 'components/common/Input/Input';
import ControlledSelect from 'components/common/Select/ControlledSelect';
import { matchTypeOptions } from 'components/ACLPage/Form/constants';
import useAppParams from 'lib/hooks/useAppParams';
import { ClusterName } from 'redux/interfaces';
import * as S from 'components/ACLPage/Form/Form.styled';
import ACLFormContext from 'components/ACLPage/Form/AclFormContext';
import { AclDetailedFormProps } from 'components/ACLPage/Form/types';

import formSchema from './schema';
import { FormValues } from './types';
import { toRequest } from './lib';
import {
defaultValues,
operations,
permissions,
resourceTypes,
} from './constants';

const CustomACLForm: FC<AclDetailedFormProps> = ({ formRef }) => {
const context = useContext(ACLFormContext);

const methods = useForm<FormValues>({
mode: 'all',
resolver: yupResolver(formSchema),
defaultValues: { ...defaultValues },
});

const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
const create = useCreateCustomAcl(clusterName);

const onSubmit = async (data: FormValues) => {
try {
const resource = toRequest(data);
await create.createResource(resource);
context?.close();
} catch (e) {
// no custom error
}
};

return (
<FormProvider {...methods}>
<S.Form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}>
<hr />
<S.Field>
<S.Label htmlFor="principal">Principal</S.Label>
<Input
name="principal"
id="principal"
placeholder="Principal"
withError
/>
</S.Field>

<S.Field>
<S.Label htmlFor="host">Host restriction</S.Label>
<Input name="host" id="host" placeholder="Host" withError />
</S.Field>
<hr />

<S.Field>
<S.Label htmlFor="resourceType">Resource type</S.Label>
<ControlledSelect options={resourceTypes} name="resourceType" />
</S.Field>

<S.Field>
<S.Label>Operations</S.Label>
<S.ControlList>
<ControlledRadio name="permission" options={permissions} />
<ControlledSelect options={operations} name="operation" />
</S.ControlList>
</S.Field>

<S.Field>
<S.Field>Matching pattern</S.Field>
<S.ControlList>
<ControlledRadio
name="namePatternType"
options={matchTypeOptions}
/>
<Input
name="resourceName"
id="resourceName"
placeholder="Matching pattern"
withError
/>
</S.ControlList>
</S.Field>
</S.Form>
</FormProvider>
);
};

export default React.memo(CustomACLForm);
48 changes: 48 additions & 0 deletions frontend/src/components/ACLPage/Form/CustomACL/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SelectOption } from 'components/common/Select/Select';
import {
KafkaAclOperationEnum,
KafkaAclPermissionEnum,
KafkaAclResourceType,
} from 'generated-sources';
import { RadioOption } from 'components/common/Radio/types';

import { FormValues } from './types';

function toOptionsArray<T extends string>(
list: T[],
unknown: T
): SelectOption<T>[] {
return list.reduce<SelectOption<T>[]>((acc, cur) => {
if (cur !== unknown) {
acc.push({ label: cur, value: cur });
}

return acc;
}, []);
}

export const resourceTypes = toOptionsArray(
Object.values(KafkaAclResourceType),
KafkaAclResourceType.UNKNOWN
);

export const operations = toOptionsArray(
Object.values(KafkaAclOperationEnum),
KafkaAclOperationEnum.UNKNOWN
);

export const permissions: RadioOption[] = [
{
value: KafkaAclPermissionEnum.ALLOW,
itemType: 'green',
},
{
value: KafkaAclPermissionEnum.DENY,
itemType: 'red',
},
];

export const defaultValues: Partial<FormValues> = {
resourceType: resourceTypes[0].value as KafkaAclResourceType,
operation: operations[0].value as KafkaAclOperationEnum,
};
26 changes: 26 additions & 0 deletions frontend/src/components/ACLPage/Form/CustomACL/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { KafkaAcl, KafkaAclNamePatternType } from 'generated-sources';
import isRegex from 'lib/isRegex';
import { MatchType } from 'components/ACLPage/Form/types';

import { FormValues } from './types';

export function toRequest(formValue: FormValues): KafkaAcl {
let namePatternType: KafkaAclNamePatternType;
if (formValue.namePatternType === MatchType.PREFIXED) {
namePatternType = KafkaAclNamePatternType.PREFIXED;
} else if (isRegex(formValue.resourceName)) {
namePatternType = KafkaAclNamePatternType.MATCH;
} else {
namePatternType = KafkaAclNamePatternType.LITERAL;
}

return {
resourceType: formValue.resourceType,
resourceName: formValue.resourceName,
namePatternType,
principal: formValue.principal,
host: formValue.host,
operation: formValue.operation,
permission: formValue.permission,
};
}
13 changes: 13 additions & 0 deletions frontend/src/components/ACLPage/Form/CustomACL/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { object, string } from 'yup';

const formSchema = object({
resourceType: string().required(),
resourceName: string().required(),
namePatternType: string().required(),
principal: string().required(),
host: string().required(),
operation: string().required(),
permission: string().required(),
});

export default formSchema;
16 changes: 16 additions & 0 deletions frontend/src/components/ACLPage/Form/CustomACL/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
KafkaAclOperationEnum,
KafkaAclPermissionEnum,
KafkaAclResourceType,
} from 'generated-sources';
import { MatchType } from 'components/ACLPage/Form/types';

export interface FormValues {
resourceType: KafkaAclResourceType;
resourceName: string;
namePatternType: MatchType;
principal: string;
host: string;
operation: KafkaAclOperationEnum;
permission: KafkaAclPermissionEnum;
}
112 changes: 112 additions & 0 deletions frontend/src/components/ACLPage/Form/ForConsumers/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { FC, useContext } from 'react';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { ClusterName } from 'redux/interfaces';
import { useCreateConsumersAcl } from 'lib/hooks/api/acl';
import useAppParams from 'lib/hooks/useAppParams';
import ControlledMultiSelect from 'components/common/MultiSelect/ControlledMultiSelect';
import Input from 'components/common/Input/Input';
import * as S from 'components/ACLPage/Form/Form.styled';
import { AclDetailedFormProps, MatchType } from 'components/ACLPage/Form/types';
import useTopicsOptions from 'components/ACLPage/lib/useTopicsOptions';
import useConsumerGroupsOptions from 'components/ACLPage/lib/useConsumerGroupsOptions';
import ACLFormContext from 'components/ACLPage/Form/AclFormContext';
import MatchTypeSelector from 'components/ACLPage/Form/components/MatchTypeSelector';

import formSchema from './schema';
import { toRequest } from './lib';
import { FormValues } from './types';

const ForConsumersForm: FC<AclDetailedFormProps> = ({ formRef }) => {
const context = useContext(ACLFormContext);
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
const create = useCreateConsumersAcl(clusterName);
const methods = useForm<FormValues>({
mode: 'all',
resolver: yupResolver(formSchema),
});

const { setValue } = methods;

const onSubmit = async (data: FormValues) => {
try {
await create.createResource(toRequest(data));
context?.close();
} catch (e) {
// no custom error
}
};

const topics = useTopicsOptions(clusterName);
const consumerGroups = useConsumerGroupsOptions(clusterName);

const onTopicTypeChange = (value: string) => {
if (value === MatchType.EXACT) {
setValue('topicsPrefix', undefined);
} else {
setValue('topics', undefined);
}
};

const onConsumerGroupTypeChange = (value: string) => {
if (value === MatchType.EXACT) {
setValue('consumerGroupsPrefix', undefined);
} else {
setValue('consumerGroups', undefined);
}
};

return (
<FormProvider {...methods}>
<S.Form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}>
<hr />
<S.Field>
<S.Label htmlFor="principal">Principal</S.Label>
<Input
name="principal"
id="principal"
placeholder="Principal"
withError
/>
</S.Field>

<S.Field>
<S.Label htmlFor="host">Host restriction</S.Label>
<Input name="host" id="host" placeholder="Host" withError />
</S.Field>
<hr />

<S.Field>
<S.Label>From Topic(s)</S.Label>
<S.ControlList>
<MatchTypeSelector
exact={<ControlledMultiSelect name="topics" options={topics} />}
prefixed={<Input name="topicsPrefix" placeholder="Prefix..." />}
onChange={onTopicTypeChange}
/>
</S.ControlList>
</S.Field>

<S.Field>
<S.Field>Consumer group(s)</S.Field>
<S.ControlList>
<MatchTypeSelector
exact={
<ControlledMultiSelect
name="consumerGroups"
options={consumerGroups}
/>
}
prefixed={
<Input name="consumerGroupsPrefix" placeholder="Prefix..." />
}
onChange={onConsumerGroupTypeChange}
/>
</S.ControlList>
</S.Field>
</S.Form>
</FormProvider>
);
};

export default React.memo(ForConsumersForm);
14 changes: 14 additions & 0 deletions frontend/src/components/ACLPage/Form/ForConsumers/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CreateConsumerAcl } from 'generated-sources/models/CreateConsumerAcl';

import { FormValues } from './types';

export const toRequest = (formValues: FormValues): CreateConsumerAcl => {
return {
principal: formValues.principal,
host: formValues.host,
consumerGroups: formValues.consumerGroups?.map((opt) => opt.value),
consumerGroupsPrefix: formValues.consumerGroupsPrefix,
topics: formValues.topics?.map((opt) => opt.value),
topicsPrefix: formValues.topicsPrefix,
};
};
22 changes: 22 additions & 0 deletions frontend/src/components/ACLPage/Form/ForConsumers/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { array, object, string } from 'yup';

const formSchema = object({
principal: string().required(),
host: string().required(),
topics: array().of(
object().shape({
label: string().required(),
value: string().required(),
})
),
topicsPrefix: string(),
consumerGroups: array().of(
object().shape({
label: string().required(),
value: string().required(),
})
),
consumerGroupsPrefix: string(),
});

export default formSchema;
10 changes: 10 additions & 0 deletions frontend/src/components/ACLPage/Form/ForConsumers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Option } from 'react-multi-select-component';

export interface FormValues {
principal: string;
host: string;
topics?: Option[];
topicsPrefix?: string;
consumerGroups?: Option[];
consumerGroupsPrefix?: string;
}
Loading

0 comments on commit 6b7fda4

Please sign in to comment.