mirror of
https://github.com/didi/KnowStreaming.git
synced 2025-12-24 11:52:08 +08:00
490 lines
14 KiB
TypeScript
490 lines
14 KiB
TypeScript
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
import {
|
|
Button,
|
|
Form,
|
|
Input,
|
|
Modal,
|
|
message,
|
|
ProTable,
|
|
Drawer,
|
|
Space,
|
|
Divider,
|
|
AppContainer,
|
|
DKSBreadcrumb,
|
|
Utils,
|
|
Checkbox,
|
|
Tooltip,
|
|
Alert,
|
|
} from 'knowdesign';
|
|
import { IconFont } from '@knowdesign/icons';
|
|
import { CloseOutlined, EyeInvisibleOutlined, EyeOutlined, LoadingOutlined } from '@ant-design/icons';
|
|
import './index.less';
|
|
import api from '@src/api';
|
|
import { useParams } from 'react-router-dom';
|
|
import { regKafkaPassword } from '@src/constants/reg';
|
|
import { tableHeaderPrefix } from '@src/constants/common';
|
|
|
|
export const randomString = (len = 32, chars = 'abcdefghijklmnopqrstuvwxyz1234567890'): string => {
|
|
const maxPos = chars.length;
|
|
let str = '';
|
|
for (let i = 0; i < len; i++) {
|
|
str += chars.charAt(Math.floor(Math.random() * maxPos));
|
|
}
|
|
return str;
|
|
};
|
|
|
|
const { confirm } = Modal;
|
|
|
|
enum UsersOperate {
|
|
Add,
|
|
ChangePassword,
|
|
}
|
|
export type UsersProps = {
|
|
clusterId: number;
|
|
name: string;
|
|
authType: any;
|
|
authName: string;
|
|
createTime: string;
|
|
updateTime: string;
|
|
};
|
|
|
|
export const defaultPagination = {
|
|
current: 1,
|
|
pageSize: 10,
|
|
position: 'bottomRight',
|
|
showSizeChanger: true,
|
|
pageSizeOptions: ['10', '20', '50', '100'],
|
|
};
|
|
|
|
const AES_KEY = 'KnowStreamingKM$';
|
|
const initialShowPassword = '********';
|
|
|
|
const AUTH_TYPES = [
|
|
{
|
|
label: 'SCRAM',
|
|
value: 1300,
|
|
},
|
|
{
|
|
label: 'GSSAPI',
|
|
value: 'gssapi',
|
|
disabled: true,
|
|
},
|
|
{
|
|
label: 'PLAIN',
|
|
value: 'plain',
|
|
disabled: true,
|
|
},
|
|
{
|
|
label: 'OAUTHBEARER',
|
|
value: 'oauthbearer',
|
|
disabled: true,
|
|
},
|
|
];
|
|
|
|
const PasswordContent = (props: { clusterId: string; name: string }) => {
|
|
const { clusterId, name } = props;
|
|
const [loading, setLoading] = useState(false);
|
|
const [pw, setPw] = useState(initialShowPassword);
|
|
|
|
const switchPwStatus = () => {
|
|
if (!loading) {
|
|
setLoading(true);
|
|
if (pw === initialShowPassword) {
|
|
Utils.request(api.getKafkaUserToken(clusterId, name)).then(
|
|
(res: any) => {
|
|
let token = res.token || '';
|
|
if (res?.decrypt) {
|
|
token = Utils.decryptAES(token, AES_KEY);
|
|
}
|
|
setPw(token);
|
|
setLoading(false);
|
|
},
|
|
() => setLoading(false)
|
|
);
|
|
} else {
|
|
setPw(initialShowPassword);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
<Tooltip title={pw} placement="bottom">
|
|
<div style={{ maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{pw}</div>
|
|
</Tooltip>
|
|
<span style={{ marginLeft: 6 }} onClick={switchPwStatus}>
|
|
{loading ? <LoadingOutlined /> : pw === initialShowPassword ? <EyeInvisibleOutlined /> : <EyeOutlined />}
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 新增 KafkaUser / 修改 KafkaUser 密码
|
|
// eslint-disable-next-line react/display-name
|
|
const EditKafkaUser = forwardRef((_, ref) => {
|
|
const { clusterId } = useParams<{
|
|
clusterId: string;
|
|
}>();
|
|
const [type, setType] = useState<UsersOperate>(UsersOperate.Add);
|
|
const [form] = Form.useForm();
|
|
const [visible, setVisible] = useState<boolean>(false);
|
|
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
|
const callback = useRef(() => {
|
|
return;
|
|
});
|
|
|
|
// 提交表单
|
|
const onSubmit = () => {
|
|
form.validateFields().then((formData) => {
|
|
// 对密码进行加密
|
|
formData.token = Utils.encryptAES(formData.token, AES_KEY);
|
|
// 注意:目前 authType 字段固定传 SCRAM 认证方式的值,之后如果开通了多认证方式,这里需要做更改
|
|
formData.authType = 1300;
|
|
setConfirmLoading(true);
|
|
Utils.request(type === UsersOperate.Add ? api.kafkaUser : api.updateKafkaUserToken, {
|
|
method: type === UsersOperate.Add ? 'POST' : 'PUT',
|
|
data: Object.assign(formData, { clusterId: Number(clusterId) }),
|
|
}).then(
|
|
() => {
|
|
// 执行回调,刷新列表数据
|
|
callback.current();
|
|
|
|
onClose();
|
|
message.success(type === UsersOperate.Add ? '成功新增 KafkaUser' : '成功修改密码');
|
|
},
|
|
() => setConfirmLoading(false)
|
|
);
|
|
});
|
|
};
|
|
|
|
// 展开抽屉
|
|
const onOpen = (status: boolean, type: UsersOperate, cbk: () => void, detail: UsersProps) => {
|
|
detail && form.setFieldsValue({ kafkaUser: detail.name });
|
|
setType(type);
|
|
setVisible(status);
|
|
callback.current = cbk;
|
|
};
|
|
|
|
// 关闭抽屉
|
|
const onClose = () => {
|
|
setVisible(false);
|
|
setConfirmLoading(false);
|
|
form.resetFields();
|
|
};
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
onOpen,
|
|
}));
|
|
|
|
return (
|
|
<Drawer
|
|
className="users-edit-drawer"
|
|
title={type === UsersOperate.Add ? '新增 KafkaUser' : '修改密码'}
|
|
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>
|
|
}
|
|
>
|
|
{type === UsersOperate.Add && (
|
|
<Alert
|
|
className="drawer-alert-full-screen"
|
|
message="新增 KafkaUser 必须在集群已开启 ACL 功能时才会生效"
|
|
type="info"
|
|
showIcon
|
|
style={{ marginBottom: 20 }}
|
|
/>
|
|
)}
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item
|
|
label="KafkaUser"
|
|
name="kafkaUser"
|
|
rules={[
|
|
{ required: true, message: 'KafkaUser 不能为空' },
|
|
{ pattern: /^[a-zA-Z0-9]{3,128}$/, message: 'KafkaUser 支持大小写英文、数字,长度为 3~128 字符' },
|
|
]}
|
|
>
|
|
<Input placeholder="请输入 KakfaUser" disabled={type === UsersOperate.ChangePassword} />
|
|
</Form.Item>
|
|
{type === UsersOperate.Add && (
|
|
<Form.Item
|
|
label="认证方式"
|
|
name="authType"
|
|
rules={[{ required: true, message: '认证方式不能为空' }]}
|
|
initialValue={[AUTH_TYPES[0].value]}
|
|
>
|
|
<Checkbox.Group>
|
|
{AUTH_TYPES.map((type) => {
|
|
return type.disabled ? (
|
|
<Tooltip title="当前版本暂不支持">
|
|
<Checkbox value={type.value} disabled>
|
|
{type.label}
|
|
</Checkbox>
|
|
</Tooltip>
|
|
) : (
|
|
<Checkbox value={type.value}>{type.label}</Checkbox>
|
|
);
|
|
})}
|
|
</Checkbox.Group>
|
|
</Form.Item>
|
|
)}
|
|
<Form.Item
|
|
label={
|
|
<div className="complex-label">
|
|
<span>{type === UsersOperate.ChangePassword && 'New'} Password</span>
|
|
<span>
|
|
<Tooltip title={'生成随机内容'} placement="left">
|
|
<IconFont
|
|
type="icon-shengchengdaima"
|
|
className="random-icon"
|
|
onClick={() => {
|
|
const randomStr = randomString(
|
|
12,
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-!"#$%&\'()*+,./:;<=>?@[]^`{|}~'
|
|
);
|
|
form && form.setFieldsValue({ token: randomStr });
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
</span>
|
|
</div>
|
|
}
|
|
name="token"
|
|
rules={[
|
|
{ required: true, message: 'Password 不能为空' },
|
|
{
|
|
pattern: regKafkaPassword,
|
|
message: '密码支持中英文、数字、特殊字符 ! " # $ % & \' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~',
|
|
},
|
|
]}
|
|
>
|
|
<Input.Password placeholder="请输入密码" maxLength={128} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Drawer>
|
|
);
|
|
});
|
|
|
|
const SecurityUsers = (): JSX.Element => {
|
|
const [global] = AppContainer.useGlobalValue();
|
|
const { clusterId } = useParams<{
|
|
clusterId: string;
|
|
}>();
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [data, setData] = useState<UsersProps[]>([]);
|
|
const [pagination, setPagination] = useState<any>(defaultPagination);
|
|
const [searchKeywords, setSearchKeywords] = useState('');
|
|
const [searchKeywordsInput, setSearchKeywordsInput] = useState('');
|
|
const editDrawerRef = useRef(null);
|
|
|
|
const getKafkaUserList = (query = {}) => {
|
|
const queryParams = {
|
|
pageNo: pagination.current,
|
|
pageSize: pagination.pageSize,
|
|
searchKeywords,
|
|
...query,
|
|
};
|
|
setLoading(true);
|
|
Utils.request(api.getKafkaUsers(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) {
|
|
getKafkaUserList({ pageNo: pages });
|
|
return false;
|
|
}
|
|
|
|
setPagination({
|
|
...pagination,
|
|
current: pageNo,
|
|
pageSize,
|
|
total,
|
|
});
|
|
setData(res.bizData);
|
|
setLoading(false);
|
|
return true;
|
|
},
|
|
() => setLoading(false)
|
|
);
|
|
};
|
|
|
|
const columns = () => {
|
|
const baseColumns = [
|
|
{
|
|
title: 'KafkaUser',
|
|
dataIndex: 'name',
|
|
width: 300,
|
|
render(name: string) {
|
|
return (
|
|
<Tooltip title={name} placement="topLeft">
|
|
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
|
|
</Tooltip>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '认证方式',
|
|
dataIndex: 'authName',
|
|
},
|
|
{
|
|
title: 'Password',
|
|
dataIndex: 'token',
|
|
width: 300,
|
|
render(_: string, record: UsersProps) {
|
|
return <PasswordContent clusterId={clusterId} name={record.name} />;
|
|
},
|
|
},
|
|
{
|
|
title: '操作',
|
|
dataIndex: '',
|
|
width: 240,
|
|
render(record: UsersProps) {
|
|
return (
|
|
<>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
style={{ paddingLeft: 0 }}
|
|
onClick={() => editDrawerRef.current.onOpen(true, UsersOperate.ChangePassword, getKafkaUserList, record)}
|
|
>
|
|
修改密码
|
|
</Button>
|
|
<Button type="link" size="small" onClick={() => onDelete(record)}>
|
|
删除
|
|
</Button>
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
return baseColumns;
|
|
};
|
|
|
|
const onDelete = (record: UsersProps) => {
|
|
confirm({
|
|
title: '确定删除该 KafkaUser 吗?',
|
|
okText: '删除',
|
|
okType: 'primary',
|
|
centered: true,
|
|
okButtonProps: {
|
|
size: 'small',
|
|
danger: true,
|
|
},
|
|
cancelButtonProps: {
|
|
size: 'small',
|
|
},
|
|
maskClosable: false,
|
|
onOk() {
|
|
return Utils.request(api.kafkaUser, {
|
|
method: 'DELETE',
|
|
data: {
|
|
clusterId,
|
|
kafkaUser: record.name,
|
|
},
|
|
}).then((_) => {
|
|
message.success('删除成功');
|
|
getKafkaUserList();
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const onTableChange = (curPagination: any) => {
|
|
getKafkaUserList({ pageNo: curPagination.current, pageSize: curPagination.pageSize });
|
|
};
|
|
|
|
useEffect(() => {
|
|
// 获取配置列表
|
|
getKafkaUserList();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
(searchKeywords || searchKeywords === '') && getKafkaUserList({ pageNo: 1 });
|
|
}, [searchKeywords]);
|
|
|
|
return (
|
|
<div className="security-users-page">
|
|
<DKSBreadcrumb
|
|
breadcrumbs={[
|
|
{ label: '多集群管理', aHref: '/' },
|
|
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
|
{ label: 'Users', aHref: `` },
|
|
]}
|
|
/>
|
|
<div className="security-users-page-list">
|
|
<div className={tableHeaderPrefix}>
|
|
<div className={`${tableHeaderPrefix}-left`}>
|
|
<div className={`${tableHeaderPrefix}-left-refresh`} onClick={() => getKafkaUserList()}>
|
|
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
|
|
</div>
|
|
</div>
|
|
<div className={`${tableHeaderPrefix}-right`}>
|
|
<Input
|
|
className="search-input"
|
|
suffix={
|
|
<IconFont
|
|
type="icon-fangdajing"
|
|
onClick={(_) => {
|
|
setSearchKeywords(searchKeywordsInput);
|
|
}}
|
|
style={{ fontSize: '16px' }}
|
|
/>
|
|
}
|
|
placeholder="请输入 Kafka User"
|
|
value={searchKeywordsInput}
|
|
onPressEnter={(_) => {
|
|
setSearchKeywords(searchKeywordsInput);
|
|
}}
|
|
onChange={(e) => {
|
|
setSearchKeywordsInput(e.target.value);
|
|
}}
|
|
/>
|
|
<Button
|
|
type="primary"
|
|
// icon={<PlusOutlined />}
|
|
onClick={() => editDrawerRef.current.onOpen(true, UsersOperate.Add, getKafkaUserList)}
|
|
>
|
|
新增KafkaUser
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<ProTable
|
|
tableProps={{
|
|
showHeader: false,
|
|
loading,
|
|
rowKey: 'name',
|
|
dataSource: data,
|
|
paginationProps: pagination,
|
|
columns: columns() as any,
|
|
lineFillColor: true,
|
|
attrs: {
|
|
onChange: onTableChange,
|
|
scroll: { y: 'calc(100vh - 270px)' },
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<EditKafkaUser ref={editDrawerRef} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SecurityUsers;
|