初始化3.0.0版本

This commit is contained in:
zengqiao
2022-08-18 17:04:05 +08:00
parent 462303fca0
commit 51832385b1
2446 changed files with 93177 additions and 127211 deletions

View File

@@ -0,0 +1,420 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Button, Form, Input, Select, message, Drawer, Space, Divider, Utils, Radio, AutoComplete, Alert } from 'knowdesign';
import api from '@src/api';
import { useParams } from 'react-router-dom';
import { UsersProps } from '../SecurityUsers';
// 字段对应后端存储值的枚举类型
export enum ACL_OPERATION {
Unknown,
Any,
All,
Read,
Write,
Create,
Delete,
Alter,
Describe,
ClusterAction,
DescribeConfigs,
AlterConfigs,
IdempotentWrite,
}
export enum ACL_PERMISSION_TYPE {
Unknown,
Any,
Deny,
Allow,
}
export enum ACL_PATTERN_TYPE {
Unknown,
Any,
Match,
Literal,
Prefixed,
}
export enum ACL_RESOURCE_TYPE {
Unknown,
Any,
Topic,
Group,
Cluster,
TransactionalId,
DelegationToken,
}
export type RESOURCE_MAP_KEYS = Exclude<keyof typeof ACL_RESOURCE_TYPE, 'Unknown' | 'Any' | 'DelegationToken'>;
// 资源类型和操作映射表
export const RESOURCE_TO_OPERATIONS_MAP: {
[P in RESOURCE_MAP_KEYS]: string[];
} = {
Cluster: ['Alter', 'AlterConfigs', 'ClusterAction', 'Create', 'Describe', 'DescribeConfigs', 'IdempotentWrite'],
Topic: ['Alter', 'AlterConfigs', 'Create', 'Delete', 'Describe', 'DescribeConfigs', 'Read', 'Write'],
Group: ['Delete', 'Describe', 'Read'],
TransactionalId: ['Write', 'Describe'],
};
// ACL 配置类型
const CONFIG_TYPE = [
{
label: '配置生产权限',
value: 'produce',
},
{
label: '配置消费权限',
value: 'consume',
},
{
label: '配置自定义权限',
value: 'custom',
},
];
// eslint-disable-next-line react/display-name
const AddDrawer = forwardRef((_, ref) => {
const { clusterId } = useParams<{
clusterId: string;
}>();
const [form] = Form.useForm();
const [visible, setVisible] = useState<boolean>(false);
const [kafkaUserOptions, setKafkaUserOptions] = useState<{ label: string; value: string }[]>([]);
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
const callback = useRef(() => {
return;
});
const [topicMetaData, setTopicMetaData] = React.useState([]);
// 获取 Topic 元信息
const getTopicMetaData = (newValue: any) => {
Utils.request(api.getTopicMetaData(+clusterId), {
method: 'GET',
params: { searchKeyword: newValue },
}).then((res: UsersProps[]) => {
const topics = (res || []).map((item: any) => {
return {
label: item.topicName,
value: item.topicName,
};
});
setTopicMetaData(topics);
});
};
// 获取 kafkaUser 列表
const getKafkaUserList = () => {
Utils.request(api.getKafkaUsers(clusterId), {
method: 'GET',
}).then((res: UsersProps[]) => {
setKafkaUserOptions(res.map(({ name }) => ({ label: name, value: name })));
});
};
// 提交表单
const onSubmit = () => {
form.validateFields().then((formData) => {
const submitData = [];
const { configType, principle, kafkaUser } = formData;
if (configType === 'custom') {
// 1. 自定义权限
const { resourceType, resourcePatternType, aclPermissionType, aclOperation, aclClientHost } = formData;
submitData.push({
clusterId,
kafkaUser: principle === 'all' ? '*' : kafkaUser,
resourceType,
resourcePatternType,
resourceName: '*',
aclPermissionType,
aclOperation,
aclClientHost,
});
} else {
// 2. 生产或者消费权限
// 1). 配置生产权限将赋予 User 对应 Topic 的 Create、Write 权限
// 2). 配置消费权限将赋予 User 对应 Topic的 Read 权限和 Group 的 Read 权限
const { topicPatternType, topicPrinciple, topicName } = formData;
submitData.push({
clusterId,
kafkaUser: principle === 'all' ? '*' : kafkaUser,
resourceType: ACL_RESOURCE_TYPE.Topic,
resourcePatternType: topicPatternType,
resourceName: topicPrinciple === 'all' ? '*' : topicName,
aclPermissionType: ACL_PERMISSION_TYPE.Allow,
aclOperation: configType === 'consume' ? ACL_OPERATION.Read : ACL_OPERATION.Create,
aclClientHost: '*',
});
// 消费权限
if (configType === 'consume') {
const { groupPatternType, groupPrinciple, groupName } = formData;
submitData.push({
clusterId,
kafkaUser: principle === 'all' ? '*' : kafkaUser,
resourceType: ACL_RESOURCE_TYPE.Group,
resourcePatternType: groupPatternType,
resourceName: groupPrinciple === 'all' ? '*' : groupName,
aclPermissionType: ACL_PERMISSION_TYPE.Allow,
aclOperation: ACL_OPERATION.Read,
aclClientHost: '*',
});
} else {
submitData.push({
clusterId,
kafkaUser: principle === 'all' ? '*' : kafkaUser,
resourceType: ACL_RESOURCE_TYPE.Topic,
resourcePatternType: topicPatternType,
resourceName: topicPrinciple === 'all' ? '*' : topicName,
aclPermissionType: ACL_PERMISSION_TYPE.Allow,
aclOperation: ACL_OPERATION.Write,
aclClientHost: '*',
});
}
}
setConfirmLoading(true);
Utils.request(api.addACL, {
method: 'POST',
data: submitData,
}).then(
() => {
// 执行回调,刷新列表数据
callback.current();
onClose();
message.success('成功新增 ACL');
},
() => setConfirmLoading(false)
);
});
};
// 展开抽屉
const onOpen = (status: boolean, cbk: () => void) => {
setVisible(status);
callback.current = cbk;
};
// 关闭抽屉
const onClose = () => {
setVisible(false);
setConfirmLoading(false);
form.resetFields();
};
useImperativeHandle(ref, () => ({
onOpen,
}));
useEffect(() => {
getKafkaUserList();
getTopicMetaData('');
}, []);
return (
<Drawer
className="acls-edit-drawer"
title="新增ACL"
width={480}
visible={visible}
maskClosable={false}
onClose={onClose}
extra={
<Space>
<Button size="small" onClick={onClose}>
</Button>
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
</Button>
<Divider type="vertical" />
</Space>
}
>
<Alert
className="drawer-alert-full-screen"
message="新增 ACL 必须在集群已开启 ACL 功能时才会生效"
type="info"
showIcon
style={{ marginBottom: 20 }}
/>
<Form form={form} layout="vertical">
<Form.Item
label="ACL用途"
name="configType"
rules={[{ required: true, message: 'ACL用途不能为空' }]}
initialValue={CONFIG_TYPE[0].value}
>
<Select options={CONFIG_TYPE} />
</Form.Item>
<Form.Item label="Principle" name="principle" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="all">
<Radio.Group>
<Radio value="all">ALL</Radio>
<Radio value="special">Special</Radio>
</Radio.Group>
</Form.Item>
<Form.Item dependencies={['principle']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) =>
getFieldValue('principle') === 'special' ? (
<Form.Item name="kafkaUser" rules={[{ required: true, message: 'Kafka User 不能为空' }]}>
<Select placeholder="请选择 Kafka User" options={kafkaUserOptions} />
</Form.Item>
) : null
}
</Form.Item>
<Form.Item dependencies={['configType']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) => {
const PatternTypeFormItems = (props: { type: string }) => {
const { type } = props;
const UpperCaseType = type[0].toUpperCase() + type.slice(1);
return (
<div className="form-item-group">
<Form.Item
label={`${UpperCaseType} Pattern Type`}
name={`${type}PatternType`}
rules={[{ required: true, message: `${UpperCaseType} Pattern Type 不能为空` }]}
initialValue={ACL_PATTERN_TYPE['Literal']}
>
<Radio.Group>
<Radio value={ACL_PATTERN_TYPE['Literal']}>Literal</Radio>
<Radio value={ACL_PATTERN_TYPE['Prefixed']}>Prefixed</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label={UpperCaseType}
name={`${type}Principle`}
rules={[{ required: true, message: `${UpperCaseType} 不能为空` }]}
initialValue="all"
>
<Radio.Group>
<Radio value="all">ALL</Radio>
<Radio value="special">Special</Radio>
</Radio.Group>
</Form.Item>
<Form.Item dependencies={[`${type}Principle`]} style={{ marginBottom: 0 }}>
{({ getFieldValue }) =>
getFieldValue(`${type}Principle`) === 'special' ? (
<Form.Item
name={`${type}Name`}
dependencies={[`${type}PatternType`]}
validateTrigger="onBlur"
rules={[
({ getFieldValue }) => ({
validator: (rule: any, value: string) => {
if (!value) {
return Promise.reject(`${UpperCaseType}Name 不能为空`);
}
if (type === 'topic' && getFieldValue(`${type}PatternType`) === ACL_PATTERN_TYPE['Literal']) {
return Utils.request(api.getTopicMetadata(clusterId as any, value)).then((res: any) => {
return res?.exist ? Promise.resolve() : Promise.reject('该 Topic 不存在');
});
}
return Promise.resolve();
},
}),
]}
>
<AutoComplete
filterOption={(value, option) => {
if (option?.value.includes(value)) {
return true;
}
return false;
}}
options={topicMetaData}
placeholder={`请输入 ${type}Name`}
/>
</Form.Item>
) : null
}
</Form.Item>
</div>
);
};
const CustomFormItems = () => {
return (
<>
<Form.Item
label="Permission Type"
name="aclPermissionType"
rules={[{ required: true, message: 'Permission Type 不能为空' }]}
initialValue={ACL_PERMISSION_TYPE['Allow']}
>
<Radio.Group>
<Radio value={ACL_PERMISSION_TYPE['Allow']}>Allow</Radio>
<Radio value={ACL_PERMISSION_TYPE['Deny']}>Deny</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label="Pattern Type"
name="resourcePatternType"
rules={[{ required: true, message: 'Pattern Type 不能为空' }]}
initialValue={ACL_PATTERN_TYPE['Literal']}
>
<Radio.Group>
<Radio value={ACL_PATTERN_TYPE['Literal']}>Literal</Radio>
<Radio value={ACL_PATTERN_TYPE['Prefixed']}>Prefixed</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label="Resource Type"
name="resourceType"
rules={[{ required: true, message: 'Resource Type 不能为空' }]}
initialValue={ACL_RESOURCE_TYPE['Cluster']}
>
<Select
placeholder="请选择 Resource Type"
options={Object.keys(RESOURCE_TO_OPERATIONS_MAP).map((type: RESOURCE_MAP_KEYS) => ({
label: type,
value: ACL_RESOURCE_TYPE[type],
}))}
/>
</Form.Item>
<Form.Item dependencies={['resourceType']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) => {
form.resetFields(['aclOperation']);
return (
<Form.Item label="Operation" name="aclOperation" rules={[{ required: true, message: 'Operation 不能为空' }]}>
<Select
placeholder="请选择 Resource Type"
options={RESOURCE_TO_OPERATIONS_MAP[ACL_RESOURCE_TYPE[getFieldValue('resourceType')] as RESOURCE_MAP_KEYS].map(
(type) => ({
label: type,
value: ACL_OPERATION[type as keyof typeof ACL_OPERATION],
})
)}
/>
</Form.Item>
);
}}
</Form.Item>
<Form.Item label="Host" name="aclClientHost" initialValue="*">
<Input />
</Form.Item>
</>
);
};
const type = getFieldValue('configType');
if (type === 'produce') {
return <PatternTypeFormItems type="topic" />;
} else if (type === 'consume') {
return (
<>
<PatternTypeFormItems type="topic" />
<PatternTypeFormItems type="group" />
</>
);
} else if (type === 'custom') {
return <CustomFormItems />;
} else {
return null;
}
}}
</Form.Item>
</Form>
</Drawer>
);
});
export default AddDrawer;

View File

@@ -0,0 +1,30 @@
.security-acls-page {
.card-bar {
margin: 12px 0;
}
&-list {
width: 100%;
height: fit-content;
padding: 16px 24px;
background: #ffffff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
// border-radius: 12px;
}
.operate-bar {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
}
.acls-edit-drawer {
.form-item-group {
padding: 16px 20px 0 20px;
margin-bottom: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.dcloud-form-item-control-input {
min-height: 0;
}
}

View File

@@ -0,0 +1,263 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Form, Input, Select, Modal, message, ProTable, AppContainer, DKSBreadcrumb, Utils } from 'knowdesign';
import ACLsCardBar from '@src/components/CardBar/ACLsCardBar';
import api from '@src/api';
import { useParams } from 'react-router-dom';
import AddACLDrawer, {
ACL_OPERATION,
ACL_PERMISSION_TYPE,
ACL_PATTERN_TYPE,
ACL_RESOURCE_TYPE,
RESOURCE_TO_OPERATIONS_MAP,
RESOURCE_MAP_KEYS,
} from './EditDrawer';
import './index.less';
const { confirm } = Modal;
export type ACLsProps = {
kafkaUser: string;
resourceType: ACL_RESOURCE_TYPE;
resourceName: string;
resourcePatternType: ACL_PATTERN_TYPE;
aclPermissionType: ACL_PATTERN_TYPE;
aclOperation: ACL_OPERATION;
aclClientHost: string;
};
export const defaultPagination = {
current: 1,
pageSize: 10,
position: 'bottomRight',
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
};
const SecurityACLs = (): JSX.Element => {
const [global] = AppContainer.useGlobalValue();
const { clusterId } = useParams<{
clusterId: string;
}>();
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<ACLsProps[]>([]);
const [pagination, setPagination] = useState<any>(defaultPagination);
const [form] = Form.useForm();
const editDrawerRef = useRef(null);
const getACLs = (query = {}) => {
const formData = form.getFieldsValue();
const queryData = {
// 模糊查询
fuzzySearchDTOList: [] as { fieldName: string; fieldValue: string }[],
// 精确查询
preciseFilterDTOList: [] as { fieldName: string; fieldValueList: (string | number)[] }[],
};
Object.entries(formData)
.filter((i) => i[1])
.forEach(([fieldName, fieldValue]: [string, any]) => {
if (fieldName === 'resourceType') {
queryData.preciseFilterDTOList.push({
fieldName,
fieldValueList: fieldValue.map((type: string) => ACL_RESOURCE_TYPE[type as RESOURCE_MAP_KEYS]),
});
} else {
queryData.fuzzySearchDTOList.push({
fieldName,
fieldValue,
});
}
});
const queryParams = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
...queryData,
...query,
};
setLoading(true);
Utils.request(api.getACLs(clusterId), {
method: 'POST',
data: queryParams,
}).then(
(res: any) => {
const { pageNo, pageSize, total } = res.pagination;
const pages = Math.ceil(total / pageSize);
if (pageNo > pages && pages !== 0) {
getACLs({ pageNo: pages });
return false;
}
setPagination({
...pagination,
current: pageNo,
pageSize,
total,
});
setData(res.bizData);
setLoading(false);
return true;
},
() => setLoading(false)
);
};
const columns = () => {
const baseColumns = [
{
title: 'Principal',
dataIndex: 'kafkaUser',
},
{
title: 'Permission',
dataIndex: 'aclPermissionType',
render(type: number) {
return ACL_PERMISSION_TYPE[type];
},
},
{
title: 'Pattern Type',
dataIndex: 'resourcePatternType',
width: 180,
render(type: number) {
return ACL_PATTERN_TYPE[type];
},
},
{
title: 'Operations',
dataIndex: 'aclOperation',
render(type: number) {
return ACL_OPERATION[type];
},
},
{
title: 'Resource',
dataIndex: 'resourceType',
render(type: number, record: ACLsProps) {
return `${ACL_RESOURCE_TYPE[type]} ${record.resourceName}`;
},
},
{
title: 'Host',
dataIndex: 'aclClientHost',
},
{
title: '操作',
dataIndex: '',
width: 120,
render(record: ACLsProps) {
return (
<>
<Button type="link" size="small" style={{ paddingLeft: 0 }} onClick={() => onDelete(record)}>
</Button>
</>
);
},
},
];
return baseColumns;
};
const onDelete = (record: ACLsProps) => {
confirm({
title: '确定删除 ACL 吗?',
okText: '删除',
okType: 'primary',
centered: true,
okButtonProps: {
size: 'small',
danger: true,
},
cancelButtonProps: {
size: 'small',
},
onOk() {
return Utils.request(api.delACLs, {
method: 'DELETE',
data: { ...record, clusterId: Number(clusterId) },
}).then((_) => {
message.success('删除成功');
getACLs();
});
},
});
};
const onTableChange = (curPagination: any) => {
getACLs({ pageNo: curPagination.current, pageSize: curPagination.pageSize });
};
useEffect(() => {
getACLs();
}, []);
return (
<div className="security-acls-page">
<DKSBreadcrumb
breadcrumbs={[
{ label: '多集群管理', aHref: '/' },
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
{ label: 'ACLs', aHref: `` },
]}
/>
<div className="card-bar">
<ACLsCardBar />
</div>
<div className="security-acls-page-list clustom-table-content">
<div className="operate-bar">
<Form form={form} layout="inline" onFinish={() => getACLs({ page: 1 })}>
<Form.Item name="kafkaUser">
<Input placeholder="请输入 Principal" />
</Form.Item>
<Form.Item name="resourceType">
<Select
placeholder="选择 ResourceType"
options={Object.keys(RESOURCE_TO_OPERATIONS_MAP).map((key) => ({ label: key, value: key }))}
mode="multiple"
maxTagCount="responsive"
allowClear
style={{ width: 200 }}
/>
</Form.Item>
<Form.Item name="resourceName">
<Input placeholder="请输入 Resource" />
</Form.Item>
<Form.Item>
<Button type="primary" ghost htmlType="submit">
</Button>
</Form.Item>
</Form>
<Button
type="primary"
// icon={<PlusOutlined />}
onClick={() => editDrawerRef.current.onOpen(true, getACLs)}
>
ACL
</Button>
</div>
<ProTable
tableProps={{
showHeader: false,
loading,
rowKey: 'id',
dataSource: data,
paginationProps: pagination,
columns: columns() as any,
lineFillColor: true,
attrs: {
onChange: onTableChange,
scroll: { y: 'calc(100vh - 400px)' },
},
}}
/>
</div>
{/* 新增 ACL 抽屉 */}
<AddACLDrawer ref={editDrawerRef} />
</div>
);
};
export default SecurityACLs;