合并企业版分支

This commit is contained in:
ZQKC
2023-12-03 14:40:40 +08:00
98 changed files with 2129 additions and 1139 deletions

View File

@@ -16,6 +16,13 @@ const babelOptions = {
cacheDirectory: true,
babelrc: false,
presets: [require.resolve('@babel/preset-env'), require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-react')],
overrides: [
// TODO编译时需要做的事情更多应该只针对目标第三方库
{
include: './node_modules',
sourceType: 'unambiguous'
}
],
plugins: [
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
[require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }],

View File

@@ -96,7 +96,7 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
arr.push(permissions[i].id);
}
});
formData.permissionIdList = formData.permissionIdList.flat();
formData.permissionIdList = formData.permissionIdList.flat().filter((item) => item !== undefined);
setConfirmLoading(true);
request(api.editRole, {
method: type === RoleOperate.Add ? 'POST' : 'PUT',
@@ -250,7 +250,7 @@ const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
<CheckboxGroupContainer
key={i}
formInstance={form}
fieldName="permissionIdList"
fieldName={`permissionIdList`}
options={permission.options}
initSelectedOptions={initSelectedPermissions[permission.id] || []}
groupIdx={i}

View File

@@ -34,11 +34,11 @@ module.exports = {
proxy: {
'/ks-km/api/v3': {
changeOrigin: true,
target: 'https://api-kylin-xg02.intra.xiaojukeji.com/ks-km/',
target: 'http://127.0.0.1/',
},
'/logi-security/api/v1': {
changeOrigin: true,
target: 'https://api-kylin-xg02.intra.xiaojukeji.com/ks-km/',
target: 'http://127.0.0.1/',
},
},
},

View File

@@ -10004,6 +10004,12 @@
}
}
},
"pubsub-js": {
"version": "1.9.4",
"resolved": "https://registry.npmmirror.com/pubsub-js/-/pubsub-js-1.9.4.tgz",
"integrity": "sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A==",
"dev": true
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.0.tgz",

View File

@@ -82,6 +82,7 @@
"@types/lodash": "^4.14.184",
"@types/node": "^12.12.25",
"@types/pubsub-js": "^1.5.18",
"pubsub-js": "^1.5.18",
"@typescript-eslint/eslint-plugin": "4.13.0",
"@typescript-eslint/parser": "4.13.0",
"babel-eslint": "10.1.0",
@@ -108,6 +109,7 @@
"optimize-css-assets-webpack-plugin": "^5.0.1",
"prettier": "2.3.2",
"progress-bar-webpack-plugin": "^1.12.1",
"pubsub-js": "^1.9.4",
"query-string": "^7.0.1",
"react-refresh": "^0.10.0",
"react-router-dom": "5.2.1",

View File

@@ -95,7 +95,7 @@ const api = {
getApi(`/clusters/${clusterPhyId}/groups/${groupName}/partitions`),
resetGroupOffset: () => getApi('/group-offsets'),
getGroupOverview: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/groups-overview`),
deleteGroupOffset: () => getApi('/group-offsets'),
// topics列表
getTopicsList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics-overview`),
getReassignmentList: () => getApi(`/reassignment/topics-overview`),
@@ -108,6 +108,7 @@ const api = {
getTopicState: (clusterPhyId: number, topicName: string) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/state`),
getTopicMetadata: (clusterPhyId: number, topicName: string) =>
getApi(`/clusters/${clusterPhyId}/topics/${topicName}/metadata-combine-exist`),
deleteTopicData: () => getApi(`/topics/truncate-topic`),
// 最新的指标值
getMetricPointsLatest: (clusterPhyId: number) => getApi(`/physical-clusters/${clusterPhyId}/latest-metrics`),

View File

@@ -21,6 +21,7 @@ import { getLicenseInfo } from './constants/common';
import api from './api';
import ClusterContainer from './pages/index';
import ksLogo from './assets/ks-logo.png';
import {ClustersPermissionMap} from "./pages/CommonConfig";
interface ILocaleMap {
[index: string]: any;
@@ -79,6 +80,9 @@ const AppContent = (props: { setlanguage: (language: string) => void }) => {
const userInfo = localStorage.getItem('userInfo');
const [curActiveAppName, setCurActiveAppName] = useState('');
const [versionInfo, setVersionInfo] = useState<VersionInfo>();
const [global] = AppContainer.useGlobalValue();
const quickEntries=[];
useEffect(() => {
if (pathname.startsWith('/config')) {
setCurActiveAppName('config');
@@ -105,6 +109,23 @@ const AppContent = (props: { setlanguage: (language: string) => void }) => {
});
}, []);
if (global.hasPermission && global.hasPermission(ClustersPermissionMap.CLUSTERS_MANAGE_VIEW)){
quickEntries.push({
icon: <IconFont type="icon-duojiqunguanli"/>,
txt: '多集群管理',
ident: '',
active: curActiveAppName === 'cluster',
});
}
if (global.hasPermission && global.hasPermission(ClustersPermissionMap.SYS_MANAGE_VIEW)){
quickEntries.push({
icon: <IconFont type="icon-xitongguanli" />,
txt: '系统管理',
ident: 'config',
active: curActiveAppName === 'config',
});
}
return (
<DProLayout.Container
headerProps={{
@@ -115,20 +136,7 @@ const AppContent = (props: { setlanguage: (language: string) => void }) => {
),
username: userInfo ? JSON.parse(userInfo)?.userName : '',
icon: <DotChartOutlined />,
quickEntries: [
{
icon: <IconFont type="icon-duojiqunguanli" />,
txt: '多集群管理',
ident: '',
active: curActiveAppName === 'cluster',
},
{
icon: <IconFont type="icon-xitongguanli" />,
txt: '系统管理',
ident: 'config',
active: curActiveAppName === 'config',
},
],
quickEntries: quickEntries,
isFixed: false,
userDropMenuItems: [
<Menu.Item key={0}>

View File

@@ -49,7 +49,7 @@ const ConnectDetailCard = (props: { record: any }) => {
return (
<>
{
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 32, color: '#212529' }}>
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 28, color: '#212529' }}>
{Utils.firstCharUppercase(type) || '-'}
</span>
}
@@ -64,7 +64,7 @@ const ConnectDetailCard = (props: { record: any }) => {
return (
<>
{
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 32, color: stateEnum[state].color }}>
<span style={{ fontFamily: 'HelveticaNeue-Medium', fontSize: 28, color: stateEnum[state].color }}>
{Utils.firstCharUppercase(state) || '-'}
</span>
}

View File

@@ -0,0 +1,10 @@
import { useCallback, useState } from 'react';
export function useForceRefresh() {
const [refreshKey, setRefresh] = useState<number>(0);
const forceRefresh: () => void = useCallback(() => {
setRefresh((x) => x + 1);
}, []);
return [refreshKey, forceRefresh];
}

View File

@@ -7,6 +7,9 @@ import { goLogin } from '@src/constants/axiosConfig';
export enum ClustersPermissionMap {
CLUSTERS_MANAGE = '多集群管理',
CLUSTERS_MANAGE_VIEW = '多集群管理查看',
//仅用作隐藏掉系统管理菜单
SYS_MANAGE = '系统管理',
SYS_MANAGE_VIEW = '系统管理查看',
// Cluster
CLUSTER_ADD = '接入集群',
CLUSTER_DEL = '删除集群',
@@ -30,6 +33,9 @@ export enum ClustersPermissionMap {
TOPIC_CANCEL_REPLICATOR = 'Topic-详情-取消Topic复制',
// Consumers
CONSUMERS_RESET_OFFSET = 'Consumers-重置Offset',
GROUP_DELETE = 'Group-删除',
GROUP_TOPIC_DELETE = 'GroupOffset-Topic纬度删除',
GROUP_PARTITION_DELETE = 'GroupOffset-Partition纬度删除',
// Test
TEST_CONSUMER = 'Test-Consumer',
TEST_PRODUCER = 'Test-Producer',
@@ -39,6 +45,19 @@ export enum ClustersPermissionMap {
MM2_DELETE = 'MM2-删除',
MM2_RESTART = 'MM2-重启',
MM2_STOP_RESUME = 'MM2-暂停&恢复',
// Connector
CONNECTOR_ADD = 'Connector-新增',
CONNECTOR_CHANGE_CONFIG = 'Connector-编辑',
CONNECTOR_DELETE = 'Connector-删除',
CONNECTOR_RESTART = 'Connector-重启',
CONNECTOR_STOP_RESUME = 'Connector-暂停&恢复',
// Security
SECURITY_ACL_ADD = 'Security-ACL新增',
SECURITY_ACL_DELETE = 'Security-ACL删除',
SECURITY_USER_ADD = 'Security-User新增',
SECURITY_USER_DELETE = 'Security-User删除',
SECURITY_USER_EDIT_PASSWORD = 'Security-User修改密码',
SECURITY_USER_VIEW_PASSWORD = 'Security-User查看密码',
}
export interface PermissionNode {
@@ -88,6 +107,11 @@ const CommonConfig = () => {
clustersPermissions &&
clustersPermissions.childList.forEach((node: PermissionNode) => node.has && userPermissions.push(node.permissionName));
// 获取用户在系统管理拥有的权限
const configPermissions = userPermissionTree.find((sys: PermissionNode) => sys.permissionName === ClustersPermissionMap.SYS_MANAGE);
configPermissions &&
configPermissions.childList.forEach((node: PermissionNode) => node.has && userPermissions.push(node.permissionName));
const hasPermission = (permissionName: ClustersPermissionMap) => permissionName && userPermissions.includes(permissionName);
setGlobal((curState: any) => ({ ...curState, permissions: allPermissions, userPermissions, hasPermission, userInfo }));

View File

@@ -189,7 +189,14 @@ const StepFormFirst = (props: SubFormProps) => {
const result: FormConnectorConfigs = {
pluginConfig: {},
};
// 获取一份默认配置
const defaultPluginConfig: any = {};
pluginConfig.configs.forEach(({ definition }) => {
// 获取一份默认配置
defaultPluginConfig[definition.name] = definition?.defaultValue;
if (!getExistFormItems(pluginType).includes(definition.name)) {
const pluginConfigs = result.pluginConfig;
const group = definition.group || 'Others';
@@ -205,7 +212,7 @@ const StepFormFirst = (props: SubFormProps) => {
Object.keys(result).length &&
form.setFieldsValue({
configs: result,
configs: { ...result, defaultPluginConfig, editConnectorConfig: result.connectorConfig },
});
})
.finally(() => props.setSubmitLoading(false));
@@ -816,6 +823,8 @@ const StepFormFifth = (props: SubFormProps) => {
<InputNumber />
) : type.toUpperCase() === 'BOOLEAN' ? (
<Switch size="small" />
) : type.toUpperCase() === 'PASSWORD' ? (
<Input.Password />
) : (
<Input />
)}
@@ -947,7 +956,7 @@ export default forwardRef(
success?: {
connectClusterId: number;
connectorName: string;
configs: {
config: {
[key: string]: any;
};
};
@@ -955,6 +964,7 @@ export default forwardRef(
}) => void
) => {
const promises: Promise<any>[] = [];
const compareConfig = stepsFormRef.current[0].getFieldValue('configs'); // 获取步骤一的form信息
Object.values(stepsFormRef.current).forEach((form, i) => {
const promise = form
.validateFields()
@@ -985,11 +995,22 @@ export default forwardRef(
const [k, ...v] = l.split('=');
result[k] = v.join('=');
});
const editConnectorConfig = operateInfo.type === 'edit' ? compareConfig.editConnectorConfig : {}; // 编辑状态时拿到config配置
const newCompareConfig = { ...compareConfig.defaultPluginConfig, ...editConnectorConfig, ...result }; // 整合后的表单提交信息
Object.keys(newCompareConfig).forEach((item) => {
if (
newCompareConfig[item] === compareConfig.defaultPluginConfig[item] ||
newCompareConfig[item]?.toString() === compareConfig.defaultPluginConfig[item]?.toString()
) {
delete newCompareConfig[item]; // 清除默认值
}
});
callback({
success: {
connectClusterId: res[0].connectClusterId,
connectorName: result['name'],
configs: result,
config: newCompareConfig,
},
});
},
@@ -1013,7 +1034,7 @@ export default forwardRef(
curClusterName = cluster.label;
}
});
(jsonRef as any)?.onOpen(operateInfo.type, curClusterName, info.success.configs);
(jsonRef as any)?.onOpen(operateInfo.type, curClusterName, info.success.config);
onClose();
}
});
@@ -1026,9 +1047,9 @@ export default forwardRef(
setCurrentStep(info.error);
} else {
setSubmitLoading(true);
Object.entries(info.success.configs).forEach(([key, val]) => {
Object.entries(info.success.config).forEach(([key, val]) => {
if (val === null) {
delete info.success.configs[key];
delete info.success.config[key];
}
});
Utils.put(api.validateConnectorConfig, info.success).then(

View File

@@ -1,7 +1,7 @@
import api from '@src/api';
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
import customMessage from '@src/components/Message';
import { Button, Divider, Drawer, Form, message, Space, Utils } from 'knowdesign';
import { Button, Divider, Drawer, Form, message, Space, Utils, Select } from 'knowdesign';
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ConnectCluster, ConnectorPlugin, ConnectorPluginConfig, OperateInfo } from './AddConnector';
@@ -9,9 +9,8 @@ import { ConnectCluster, ConnectorPlugin, ConnectorPluginConfig, OperateInfo } f
const PLACEHOLDER = `配置格式如下
{
"connectClusterName": "", // Connect Cluster 名称
"configs": { // 具体配置项
"name": "",
"name": "", // Connect Cluster 名称
"config": { // 具体配置项
"connector.class": "",
"tasks.max": 1,
...
@@ -43,11 +42,16 @@ export default forwardRef((props: any, ref) => {
const onOpen = (type: 'create' | 'edit', connectClusterName?: string, defaultConfigs?: { [key: string]: any }) => {
if (defaultConfigs) {
setDefaultConfigs({ ...defaultConfigs, connectClusterName });
const connectorName = connectClusterName;
const connectClusterId = connectClusters.find((cluster) => cluster.label === connectClusterName).value;
form.setFieldsValue({
connectClusterId,
connectorName,
configs: JSON.stringify(
{
connectClusterName,
configs: defaultConfigs,
// connectClusterName,
name: defaultConfigs.name,
config: { ...defaultConfigs, name: undefined },
},
null,
2
@@ -63,13 +67,14 @@ export default forwardRef((props: any, ref) => {
form.validateFields().then(
(data) => {
const postData = JSON.parse(data.configs);
postData.connectorName = postData.configs.name;
postData.connectClusterId = connectClusters.find((cluster) => cluster.label === postData.connectClusterName).value;
delete postData.connectClusterName;
Object.entries(postData.configs).forEach(([key, val]) => {
postData.connectorName = postData.name;
postData.connectClusterId = data.connectClusterId;
postData.config.name = postData.name;
// delete postData.connectClusterName;
delete postData.name;
Object.entries(postData.config).forEach(([key, val]) => {
if (val === null) {
delete postData.configs[key];
delete postData.config[key];
}
});
Utils.put(api.validateConnectorConfig, postData).then(
@@ -161,6 +166,26 @@ export default forwardRef((props: any, ref) => {
}
>
<Form form={form} layout="vertical">
<Form.Item
name="connectClusterId"
label="Connect 集群"
rules={[
{
required: true,
validator(rule, value) {
if (!value) {
return Promise.reject('Connect 集群不能为空');
} else {
return Promise.resolve();
}
},
},
]}
initialValue={defaultConfigs?.connectClusterId}
className="connector-json-connectCluster"
>
<Select options={connectClusters} placeholder="请选择 Connect 集群" disabled={type === 'edit'} />
</Form.Item>
<Form.Item
name="configs"
validateTrigger="onBlur"
@@ -175,57 +200,48 @@ export default forwardRef((props: any, ref) => {
if (typeof v !== 'object') {
return Promise.reject('输入内容必须为 JSON');
}
let connectClusterId = -1;
// 校验 connectClusterName 字段
if (!v.connectClusterName) {
return Promise.reject('内容缺少 connectClusterName 字段或字段内容为空');
let connectClusterId = form.getFieldValue('connectClusterId');
// 校验 connectorName 字段
if (!v.name) {
return Promise.reject('内容缺少 name 项');
} else {
if (type === 'edit') {
if (v.connectClusterName !== defaultConfigs.connectClusterName) {
return Promise.reject('编辑模式下不允许修改 connectClusterName 字段');
}
} else {
if (!connectClusters.length) {
getConnectClusters();
return Promise.reject('connectClusterName 列表获取失败,请重试');
}
const targetConnectCluster = connectClusters.find((cluster) => cluster.label === v.connectClusterName);
if (!targetConnectCluster) {
return Promise.reject('connectClusterName 不存在,请检查');
} else {
connectClusterId = targetConnectCluster.value;
}
if (type === 'edit' && v.name !== defaultConfigs.name) {
return Promise.reject('编辑模式下不允许修改 name 字段');
}
}
if (!v.configs || typeof v.configs !== 'object') {
return Promise.reject('内容缺少 configs 字段或字段格式错误');
if (!v.config || typeof v.config !== 'object') {
return Promise.reject('内容缺少 config 字段或字段格式错误');
} else {
// 校验 connectorName 字段
if (!v.configs.name) {
return Promise.reject('configs 字段下缺少 name 项');
} else {
if (type === 'edit' && v.configs.name !== defaultConfigs.name) {
return Promise.reject('编辑模式下不允许修改 name 字段');
}
}
if (!v.configs['connector.class']) {
return Promise.reject('configs 字段下缺少 connector.class 项');
} else if (type === 'edit' && v.configs['connector.class'] !== defaultConfigs['connector.class']) {
// // 校验 connectorName 字段
// if (!v.config.name) {
// return Promise.reject('config 字段下缺少 name 项');
// } else {
// if (type === 'edit' && v.config.name !== defaultConfigs.name) {
// return Promise.reject('编辑模式下不允许修改 name 字段');
// }
// }
if (!v.config['connector.class']) {
return Promise.reject('config 字段下缺少 connector.class 项');
} else if (type === 'edit' && v.config['connector.class'] !== defaultConfigs['connector.class']) {
return Promise.reject('编辑模式下不允许修改 connector.class 字段');
}
}
if (type === 'create') {
// 校验创建时是否选择了connect集群
if (!connectClusterId) {
return Promise.reject('请先选择 Connect 集群');
}
// 异步校验 connector 名称是否重复 以及 className 是否存在
return Promise.all([
Utils.request(api.isConnectorExist(connectClusterId, v.configs.name)),
Utils.request(api.isConnectorExist(connectClusterId, v.config.name)),
Utils.request(api.getConnectorPlugins(connectClusterId)),
]).then(
([data, plugins]: [any, ConnectorPlugin[]]) => {
return data?.exist
? Promise.reject('name 与已有 Connector 重复')
: plugins.every((plugin) => plugin.className !== v.configs['connector.class'])
: plugins.every((plugin) => plugin.className !== v.config['connector.class'])
? Promise.reject('该 connectCluster 下不存在 connector.class 项配置的插件')
: Promise.resolve();
},

View File

@@ -1,8 +1,9 @@
import SmallChart from '@src/components/SmallChart';
import TagsWithHide from '@src/components/TagsWithHide';
import { Button, Tag, Tooltip, Utils, Popconfirm } from 'knowdesign';
import { Button, Tag, Tooltip, Utils, Popconfirm, AppContainer } from 'knowdesign';
import React from 'react';
import Delete from './Delete';
import { ClustersPermissionMap } from '../CommonConfig';
export const defaultPagination = {
current: 1,
pageSize: 10,
@@ -93,7 +94,8 @@ const renderLine = (record: any, metricName: string) => {
};
export const getConnectorsColumns = (arg?: any) => {
const columns = [
const [global] = AppContainer.useGlobalValue();
const columns: any = [
{
title: 'Connector Name',
dataIndex: 'connectorName',
@@ -213,7 +215,10 @@ export const getConnectorsColumns = (arg?: any) => {
return t && t.length > 0 ? <TagsWithHide placement="bottom" list={t} expandTagContent={(num: any) => `共有${num}`} /> : '-';
},
},
{
];
if (global.hasPermission) {
columns.push({
title: '操作',
dataIndex: 'options',
key: 'options',
@@ -224,20 +229,24 @@ export const getConnectorsColumns = (arg?: any) => {
render: (_t: any, r: any) => {
return (
<div>
<Popconfirm
title="是否重启当前任务?"
onConfirm={() => arg?.optionConnect(r, 'restart')}
// onCancel={cancel}
okText="是"
cancelText=""
overlayClassName="connect-popconfirm"
>
<Button key="restart" type="link" size="small">
</Button>
</Popconfirm>
{global.hasPermission(ClustersPermissionMap.CONNECTOR_RESTART) ? (
<Popconfirm
title="是否重启当前任务?"
onConfirm={() => arg?.optionConnect(r, 'restart')}
// onCancel={cancel}
okText=""
cancelText="否"
overlayClassName="connect-popconfirm"
>
<Button key="restart" type="link" size="small">
</Button>
</Popconfirm>
) : (
<></>
)}
{(r.state === 'RUNNING' || r.state === 'PAUSED') && (
{global.hasPermission(ClustersPermissionMap.CONNECTOR_STOP_RESUME) && (r.state === 'RUNNING' || r.state === 'PAUSED') && (
<Popconfirm
title={`是否${r.state === 'RUNNING' ? '暂停' : '继续'}当前任务?`}
onConfirm={() => arg?.optionConnect(r, r.state === 'RUNNING' ? 'stop' : 'resume')}
@@ -252,16 +261,24 @@ export const getConnectorsColumns = (arg?: any) => {
</Button>
</Popconfirm>
)}
{global.hasPermission(ClustersPermissionMap.CONNECTOR_CHANGE_CONFIG) ? (
<Button type="link" size="small" onClick={() => arg?.editConnector(r)}>
</Button>
) : (
<></>
)}
<Button type="link" size="small" onClick={() => arg?.editConnector(r)}>
</Button>
<Delete record={r} onConfirm={arg?.deleteTesk}></Delete>
{global.hasPermission(ClustersPermissionMap.CONNECTOR_DELETE) ? (
<Delete record={r} onConfirm={arg?.deleteTesk}></Delete>
) : (
<></>
)}
</div>
);
},
},
];
});
}
return columns;
};
@@ -298,6 +315,7 @@ export const getWorkersColumns = (arg?: any) => {
// Detail
export const getConnectorsDetailColumns = (arg?: any) => {
const [global] = AppContainer.useGlobalValue();
const columns = [
{
title: 'Task ID',
@@ -346,16 +364,20 @@ export const getConnectorsDetailColumns = (arg?: any) => {
render: (_t: any, r: any) => {
return (
<div>
<Popconfirm
title="是否重试当前任务?"
onConfirm={() => arg?.retryOption(r.taskId)}
// onCancel={cancel}
okText="是"
cancelText=""
overlayClassName="connect-popconfirm"
>
<a></a>
</Popconfirm>
{global.hasPermission(ClustersPermissionMap.CONNECTOR_RESTART) ? (
<Popconfirm
title="是否重试当前任务?"
onConfirm={() => arg?.retryOption(r.taskId)}
// onCancel={cancel}
okText=""
cancelText="否"
overlayClassName="connect-popconfirm"
>
<a></a>
</Popconfirm>
) : (
<></>
)}
</div>
);
},

View File

@@ -185,9 +185,10 @@
.operate-connector-drawer-use-json {
.CodeMirror.cm-s-default {
height: calc(100vh - 146px);
height: calc(100vh - 196px);
}
.dcloud-form-item {
margin-top: 16px;
margin-bottom: 0 !important;
}
}

View File

@@ -12,6 +12,7 @@ import notification from '@src/components/Notification';
import './index.less';
import AddConnectorUseJSON from './AddConnectorUseJSON';
import HasConnector from './HasConnector';
import { ClustersPermissionMap } from '../CommonConfig';
const { request } = Utils;
const rateMap: any = {
@@ -174,21 +175,25 @@ const Connectors: React.FC = () => {
maxLength: 128,
}}
/>
<span className="add-connect">
<Button
className="add-connect-btn"
icon={<IconFont type="icon-jiahao" />}
type="primary"
onClick={() => addConnectorRef.current?.onOpen('create', addConnectorJsonRef.current)}
>
Connector
</Button>
<Dropdown overlayClassName="add-connect-dropdown-menu" overlay={menu}>
<Button className="add-connect-json" type="primary">
<IconFont type="icon-guanwangxiala" />
{global.hasPermission && global.hasPermission(ClustersPermissionMap.CONNECTOR_ADD) ? (
<span className="add-connect">
<Button
className="add-connect-btn"
icon={<IconFont type="icon-jiahao" />}
type="primary"
onClick={() => addConnectorRef.current?.onOpen('create', addConnectorJsonRef.current)}
>
Connector
</Button>
</Dropdown>
</span>
<Dropdown overlayClassName="add-connect-dropdown-menu" overlay={menu}>
<Button className="add-connect-json" type="primary">
<IconFont type="icon-guanwangxiala" />
</Button>
</Dropdown>
</span>
) : (
<></>
)}
</div>
</div>
<ProTable

View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { Button, Form, Input, Modal, Utils } from 'knowdesign';
import notification from '@src/components/Notification';
import Api from '@src/api/index';
// eslint-disable-next-line react/display-name
export default (props: { record: any; onConfirm?: () => void }) => {
const { record, onConfirm } = props;
const routeParams = useParams<{
clusterId: string;
}>();
const [form] = Form.useForm();
const [delDialogVisible, setDelDialogVisble] = useState(false);
const handleDelOk = () => {
form.validateFields().then((e) => {
const formVal = form.getFieldsValue();
formVal.clusterPhyId = Number(routeParams.clusterId);
formVal.deleteType = 0;
Utils.delete(Api.deleteGroupOffset(), { data: formVal }).then((res: any) => {
if (res === null) {
notification.success({
message: '删除消费组成功',
});
setDelDialogVisble(false);
onConfirm && onConfirm();
} else {
notification.error({
message: '删除消费组失败',
});
}
});
});
};
return (
<>
<Button
style={{ paddingLeft: 0 }}
type="link"
onClick={(_) => {
setDelDialogVisble(true);
}}
>
</Button>
<Modal
className="custom-modal"
title="确定要删除此Topic吗"
centered={true}
visible={delDialogVisible}
wrapClassName="del-topic-modal"
destroyOnClose={true}
maskClosable={false}
onOk={handleDelOk}
onCancel={(_) => {
setDelDialogVisble(false);
}}
okText="删除"
okButtonProps={{
danger: true,
size: 'small',
style: {
paddingLeft: '16px',
paddingRight: '16px',
},
}}
cancelButtonProps={{
size: 'small',
style: {
paddingLeft: '16px',
paddingRight: '16px',
},
}}
>
{/* <div className="tip-info">
<IconFont type="icon-warning-circle"></IconFont>
<span>会删除Topic的全部消息数据和ACL权限请再次输入Topic名称进行确认</span>
</div> */}
<Form form={form} labelCol={{ span: 5 }} style={{ marginTop: 18 }}>
<Form.Item label="TopicName">{record.name}</Form.Item>
<Form.Item
name="groupName"
label="GroupName"
rules={[
// { required: true },
() => ({
validator(_, value) {
if (!value) {
return Promise.reject(new Error('请输入Group名称'));
} else if (value !== record.name) {
return Promise.reject(new Error('请输入正确的Group名称'));
}
return Promise.resolve();
},
}),
]}
>
<Input placeholder="请输入" size="small"></Input>
</Form.Item>
</Form>
</Modal>
</>
);
};

View File

@@ -1,12 +1,13 @@
import React, { useState, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { Drawer, ProTable, Utils } from 'knowdesign';
import { Button, Space, Divider, Drawer, ProTable, Utils, notification } from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import API from '@src/api/index';
import { defaultPagination, hashDataParse } from '@src/constants/common';
import { getGtoupTopicColumns } from './config';
import { ExpandedRow } from './ExpandedRow';
import ResetOffsetDrawer from './ResetOffsetDrawer';
import { useForceRefresh } from '@src/components/utils';
const { request } = Utils;
export interface MetricLine {
@@ -63,6 +64,7 @@ const GroupDetail = (props: any) => {
const [openKeys, setOpenKeys] = useState();
const [resetOffsetVisible, setResetOffsetVisible] = useState(false);
const [resetOffsetArg, setResetOffsetArg] = useState({});
const [refreshKey, forceRefresh] = useForceRefresh();
const genData = async ({ pageNo, pageSize, groupName }: any) => {
if (urlParams?.clusterId === undefined) return;
@@ -110,6 +112,23 @@ const GroupDetail = (props: any) => {
groupName: record?.groupName,
});
};
// 删除消费组Topic
const deleteOffset = (record: any) => {
const params = {
clusterPhyId: +urlParams?.clusterId,
deleteType: 1, // 0:group纬度1Topic纬度2Partition纬度
groupName: record.groupName,
topicName: record.topicName,
};
Utils.delete(API.deleteGroupOffset(), { data: params }).then((data: any) => {
if (data === null) {
notification.success({
message: '删除Topic成功!',
});
genData({ pageNo: 1, pageSize: pagination.pageSize, groupName: hashData.groupName });
}
});
};
const onTableChange = (pagination: any, filters: any, sorter: any) => {
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter, groupName: hashData.groupName });
@@ -160,7 +179,7 @@ const GroupDetail = (props: any) => {
// // 获取Consumer列表 表格模式
// getTopicGroupMetric(hashData);
// });
}, [hashDataParse(location.hash).groupName]);
}, [hashDataParse(location.hash).groupName, refreshKey]);
return (
<Drawer
@@ -182,6 +201,14 @@ const GroupDetail = (props: any) => {
// <Divider type="vertical" />
// </Space>
// }
extra={
<Space>
<span style={{ display: 'inline-block', fontSize: '15px' }} onClick={forceRefresh as () => void}>
<i className="iconfont icon-shuaxin1" style={{ cursor: 'pointer' }} />
</span>
<Divider type="vertical" />
</Space>
}
>
<ProTable
showQueryForm={false}
@@ -189,7 +216,7 @@ const GroupDetail = (props: any) => {
showHeader: false,
rowKey: 'key',
loading: loading,
columns: getGtoupTopicColumns({ resetOffset }),
columns: getGtoupTopicColumns({ resetOffset, deleteOffset }),
dataSource: topicData,
paginationProps: { ...pagination },
// noPagination: true,
@@ -209,6 +236,7 @@ const GroupDetail = (props: any) => {
chartData={chartData}
groupName={hashDataParse(location.hash).groupName}
loading={loadingObj}
refreshKey={refreshKey}
/>
),
// expandedRowRender,
@@ -241,7 +269,12 @@ const GroupDetail = (props: any) => {
},
}}
/>
<ResetOffsetDrawer visible={resetOffsetVisible} setVisible={setResetOffsetVisible} record={resetOffsetArg}></ResetOffsetDrawer>
<ResetOffsetDrawer
visible={resetOffsetVisible}
setVisible={setResetOffsetVisible}
record={resetOffsetArg}
resetOffsetFn={forceRefresh}
></ResetOffsetDrawer>
</Drawer>
);
};

View File

@@ -41,7 +41,7 @@ const metricWithType = [
{ metricName: 'Lag', metricType: 102 },
];
export const ExpandedRow: any = ({ record, groupName }: any) => {
export const ExpandedRow: any = ({ record, groupName, refreshKey }: any) => {
const params: any = useParams<{
clusterId: string;
}>();
@@ -193,7 +193,7 @@ export const ExpandedRow: any = ({ record, groupName }: any) => {
endTime: timeRange[1],
topNu: 0,
};
Utils.post(API.getTopicGroupMetricHistory(clusterId), params).then((data: Array<MetricData>) => {
Utils.post(API.getTopicGroupMetricHistory(clusterId), params, { timeout: 300000 }).then((data: Array<MetricData>) => {
// ! 替换接口返回
setAllGroupMetricsData(data);
});
@@ -210,10 +210,6 @@ export const ExpandedRow: any = ({ record, groupName }: any) => {
getTopicGroupMetric({ pagination, sorter });
};
// useEffect(() => {
// getTopicGroupMetric();
// }, [sortObj]);
useEffect(() => {
const hashData = hashDataParse(location.hash);
// if (!hashData.groupName) return;
@@ -242,7 +238,7 @@ export const ExpandedRow: any = ({ record, groupName }: any) => {
// 获取Consumer列表 表格模式
getTopicGroupMetric({});
});
}, [hashDataParse(location.hash).groupName]);
}, [hashDataParse(location.hash).groupName, refreshKey]);
useEffect(() => {
if (partitionList.length === 0) return;

View File

@@ -44,7 +44,7 @@ const CustomSelectResetTime = (props: { value?: string; onChange?: (val: Number
};
export default (props: any) => {
const { record, visible, setVisible } = props;
const { record, visible, setVisible, resetOffsetFn } = props;
const routeParams = useParams<{
clusterId: string;
}>();
@@ -106,6 +106,8 @@ export default (props: any) => {
message: '重置offset成功',
});
setVisible(false);
// 发布重置offset成功的消息
resetOffsetFn();
} else {
notification.error({
message: '重置offset失败',

View File

@@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import React from 'react';
import { AppContainer } from 'knowdesign';
import { AppContainer, Button, Popconfirm } from 'knowdesign';
import TagsWithHide from '@src/components/TagsWithHide';
import { ClustersPermissionMap } from '../CommonConfig';
import Delete from './Delete';
export const runningStatusEnum: any = {
1: 'Doing',
@@ -21,7 +22,8 @@ export const defaultPagination = {
};
export const getGroupColumns = (arg?: any) => {
const columns = [
const [global] = AppContainer.useGlobalValue();
const columns: any = [
{
title: 'ConsumerGroup',
dataIndex: 'name',
@@ -63,6 +65,23 @@ export const getGroupColumns = (arg?: any) => {
render: (t: number) => (t ? t.toLocaleString() : '-'),
},
];
if (global.hasPermission && global.hasPermission(ClustersPermissionMap.GROUP_DELETE)) {
columns.push({
title: '操作',
dataIndex: 'options',
key: 'options',
width: 200,
filterTitle: true,
fixed: 'right',
render: (_t: any, r: any) => {
return (
<div>
<Delete record={r} onConfirm={arg?.deleteTesk}></Delete>
</div>
);
},
});
}
return columns;
};
@@ -98,16 +117,33 @@ export const getGtoupTopicColumns = (arg?: any) => {
render: (t: number) => (t ? t.toLocaleString() : '-'),
},
];
if (global.hasPermission && global.hasPermission(ClustersPermissionMap.CONSUMERS_RESET_OFFSET)) {
if (global.hasPermission) {
columns.push({
title: '操作',
dataIndex: 'desc',
key: 'desc',
width: 150,
width: 200,
render: (value: any, record: any) => {
return (
<div>
<a onClick={() => arg.resetOffset(record)}>Offset</a>
{global.hasPermission(ClustersPermissionMap.CONSUMERS_RESET_OFFSET) ? (
<a onClick={() => arg.resetOffset(record)}>Offset</a>
) : (
<></>
)}
{global.hasPermission(ClustersPermissionMap.GROUP_TOPIC_DELETE) ? (
<Popconfirm
placement="top"
title={`是否要删除当前Topic`}
onConfirm={() => arg.deleteOffset(record)}
okText="是"
cancelText="否"
>
<Button type="link"></Button>
</Popconfirm>
) : (
<></>
)}
</div>
);
},

View File

@@ -58,6 +58,11 @@ const BrokerList: React.FC = (props: any) => {
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
};
// 删除Group
const deleteTesk = () => {
genData({ pageNo: 1, pageSize: pagination.pageSize });
};
useEffect(() => {
genData({
pageNo: 1,
@@ -115,7 +120,7 @@ const BrokerList: React.FC = (props: any) => {
showHeader: false,
rowKey: 'group_list',
loading: loading,
columns: getGroupColumns(),
columns: getGroupColumns(deleteTesk),
dataSource: data,
paginationProps: { ...pagination },
attrs: {

View File

@@ -13,7 +13,7 @@ const carouselList = [
<img className="carousel-eg-ctr-two-img img-one" src={egTwoContent} />
<div className="carousel-eg-ctr-two-desc desc-one">
<span>Github: </span>
<span>5.8K</span>
<span>6.8K</span>
<span>+ Star的的实时流处理平台</span>
</div>
<div className="carousel-eg-ctr-two-desc desc-two">

View File

@@ -185,7 +185,7 @@
.operate-connector-drawer-use-json {
.CodeMirror.cm-s-default {
height: calc(100vh - 146px);
height: calc(100vh - 196px);
}
.dcloud-form-item {
margin-bottom: 0 !important;

View File

@@ -522,28 +522,22 @@ const ConnectorForm = (props: {
const params = {
...values,
id: initFieldsValue?.id,
jmxProperties: values.jmxProperties ? `{ "jmxProperties": "${values.jmxProperties}" }` : undefined,
jmxProperties: values.jmxProperties ? `{ "jmxPort": "${values.jmxProperties}" }` : undefined,
};
Utils.put(api.batchConnectClusters, [params])
.then((res) => {
// setSelectedTabKey(undefined);
getConnectClustersList();
notification.success({
message: '修改Connect集群成功',
});
})
.catch((error) => {
notification.success({
message: '修改Connect集群失败',
});
Utils.put(api.batchConnectClusters, [params]).then((res) => {
// setSelectedTabKey(undefined);
getConnectClustersList();
notification.success({
message: '修改Connect集群成功',
});
});
};
const onCancel = () => {
setSelectedTabKey(undefined);
try {
const jmxPortInfo = JSON.parse(initFieldsValue.jmxProperties) || {};
form.setFieldsValue({ ...initFieldsValue, jmxProperties: jmxPortInfo.jmxProperties });
form.setFieldsValue({ ...initFieldsValue, jmxProperties: jmxPortInfo.jmxPort });
} catch {
form.setFieldsValue({ ...initFieldsValue });
}
@@ -552,7 +546,7 @@ const ConnectorForm = (props: {
useLayoutEffect(() => {
try {
const jmxPortInfo = JSON.parse(initFieldsValue.jmxProperties) || {};
form.setFieldsValue({ ...initFieldsValue, jmxProperties: jmxPortInfo.jmxProperties });
form.setFieldsValue({ ...initFieldsValue, jmxProperties: jmxPortInfo.jmxPort });
} catch {
form.setFieldsValue({ ...initFieldsValue });
}

View File

@@ -132,16 +132,35 @@ const AddDrawer = forwardRef((_, ref) => {
form.validateFields().then((formData) => {
const submitData = [];
const { configType, principle, kafkaUser } = formData;
if (configType === 'custom') {
// 1. 自定义权限
const { resourceType, resourcePatternType, aclPermissionType, aclOperation, aclClientHost } = formData;
// TODO: 需要和后端联调
const {
resourceType,
resourcePatternType,
aclPermissionType,
aclOperation,
aclClientHost,
cluster,
topicName,
topicPatternType,
groupName,
groupPatternType,
transactionalId,
transactionalIdPatternType,
} = formData;
submitData.push({
clusterId,
kafkaUser: principle === 'all' ? '*' : kafkaUser,
resourceType,
resourcePatternType,
resourceName: '*',
resourcePatternType: cluster
? 3
: topicPatternType
? topicPatternType
: groupPatternType
? groupPatternType
: transactionalIdPatternType,
resourceName: cluster ? cluster : topicName ? topicName : groupName ? groupName : transactionalId,
aclPermissionType,
aclOperation,
aclClientHost,
@@ -281,6 +300,42 @@ const AddDrawer = forwardRef((_, ref) => {
</Form.Item>
<Form.Item dependencies={['configType']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) => {
const SelectFormItems = (props: { type: string }) => {
const { type } = props;
return (
<Form.Item
name={`${type}Name`}
dependencies={[`${type}PatternType`]}
validateTrigger="onBlur"
rules={[
({ getFieldValue }) => ({
validator: (rule: any, value: string) => {
if (!value) {
return Promise.reject(`${type}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={type === 'topic' ? topicMetaData : groupMetaData}
placeholder={`请输入 ${type}Name`}
/>
</Form.Item>
);
};
const PatternTypeFormItems = (props: { type: string }) => {
const { type } = props;
const UpperCaseType = type[0].toUpperCase() + type.slice(1);
@@ -311,37 +366,43 @@ const AddDrawer = forwardRef((_, ref) => {
<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 不能为空`);
type !== 'transactionalId' ? (
<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;
}
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={type === 'topic' ? topicMetaData : groupMetaData}
placeholder={`请输入 ${type}Name`}
/>
</Form.Item>
return false;
}}
options={type === 'topic' ? topicMetaData : groupMetaData}
placeholder={`请输入 ${type}Name`}
/>
</Form.Item>
) : (
<Form.Item name={`transactionalId`} rules={[{ required: true, message: `TransactionalId不能为空` }]}>
<Input placeholder={`请输入TransactionalId`}></Input>
</Form.Item>
)
) : null
}
</Form.Item>
@@ -363,7 +424,7 @@ const AddDrawer = forwardRef((_, ref) => {
<Radio value={ACL_PERMISSION_TYPE['Deny']}>Deny</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
{/* <Form.Item
label="Pattern Type"
name="resourcePatternType"
rules={[{ required: true, message: 'Pattern Type 不能为空' }]}
@@ -373,7 +434,7 @@ const AddDrawer = forwardRef((_, ref) => {
<Radio value={ACL_PATTERN_TYPE['Literal']}>Literal</Radio>
<Radio value={ACL_PATTERN_TYPE['Prefixed']}>Prefixed</Radio>
</Radio.Group>
</Form.Item>
</Form.Item> */}
<Form.Item
label="Resource Type"
name="resourceType"
@@ -388,6 +449,29 @@ const AddDrawer = forwardRef((_, ref) => {
}))}
/>
</Form.Item>
<Form.Item dependencies={['resourceType']}>
{({ getFieldValue }) => {
const type = getFieldValue('resourceType');
if (type === ACL_RESOURCE_TYPE['Cluster']) {
//TODO需要和后端获取集群和事务接口联调
return (
<Form.Item
name={`${type === 4 ? 'cluster' : 'transactionalId'}`}
rules={[{ required: true, message: `${type === 4 ? 'Cluster名称' : 'TransactionalId'} 不能为空` }]}
>
<Input placeholder={`请输入${type === 4 ? 'Cluster名称' : 'TransactionalId'}`}></Input>
</Form.Item>
);
} else if (type === ACL_RESOURCE_TYPE['TransactionalId']) {
return <PatternTypeFormItems type="transactionalId" />;
} else if (type === ACL_RESOURCE_TYPE['Topic']) {
return <PatternTypeFormItems type="topic" />;
} else if (type === ACL_RESOURCE_TYPE['Group']) {
return <PatternTypeFormItems type="group" />;
}
return null;
}}
</Form.Item>
<Form.Item dependencies={['resourceType']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) => {
form.resetFields(['aclOperation']);

View File

@@ -14,6 +14,7 @@ import AddACLDrawer, {
RESOURCE_TO_OPERATIONS_MAP,
RESOURCE_MAP_KEYS,
} from './EditDrawer';
import { ClustersPermissionMap } from '../CommonConfig';
import './index.less';
const { confirm } = Modal;
@@ -105,7 +106,7 @@ const SecurityACLs = (): JSX.Element => {
};
const columns = () => {
const baseColumns = [
const baseColumns: any = [
{
title: 'Principal',
dataIndex: 'kafkaUser',
@@ -143,7 +144,9 @@ const SecurityACLs = (): JSX.Element => {
title: 'Host',
dataIndex: 'aclClientHost',
},
{
];
if (global.hasPermission && global.hasPermission(ClustersPermissionMap.SECURITY_ACL_DELETE)) {
baseColumns.push({
title: '操作',
dataIndex: '',
width: 120,
@@ -156,8 +159,8 @@ const SecurityACLs = (): JSX.Element => {
</>
);
},
},
];
});
}
return baseColumns;
};
@@ -238,15 +241,19 @@ const SecurityACLs = (): JSX.Element => {
</Form.Item>
</Form>
</div>
<div className={`${tableHeaderPrefix}-right`}>
<Button
type="primary"
// icon={<PlusOutlined />}
onClick={() => editDrawerRef.current.onOpen(true, getACLs)}
>
ACL
</Button>
</div>
{global.hasPermission && global.hasPermission(ClustersPermissionMap.SECURITY_ACL_ADD) ? (
<div className={`${tableHeaderPrefix}-right`}>
<Button
type="primary"
// icon={<PlusOutlined />}
onClick={() => editDrawerRef.current.onOpen(true, getACLs)}
>
ACL
</Button>
</div>
) : (
<></>
)}
</div>
<ProTable
tableProps={{

View File

@@ -23,7 +23,7 @@ import api from '@src/api';
import { useParams } from 'react-router-dom';
import { regKafkaPassword } from '@src/constants/reg';
import { tableHeaderPrefix } from '@src/constants/common';
import { ClustersPermissionMap } from '../CommonConfig';
export const randomString = (len = 32, chars = 'abcdefghijklmnopqrstuvwxyz1234567890'): string => {
const maxPos = chars.length;
let str = '';
@@ -85,7 +85,7 @@ const PasswordContent = (props: { clusterId: string; name: string }) => {
const { clusterId, name } = props;
const [loading, setLoading] = useState(false);
const [pw, setPw] = useState(initialShowPassword);
const [global] = AppContainer.useGlobalValue();
const switchPwStatus = () => {
if (!loading) {
setLoading(true);
@@ -113,9 +113,13 @@ const PasswordContent = (props: { clusterId: string; name: string }) => {
<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>
{global.hasPermission(ClustersPermissionMap.SECURITY_USER_VIEW_PASSWORD) ? (
<span style={{ marginLeft: 6 }} onClick={switchPwStatus}>
{loading ? <LoadingOutlined /> : pw === initialShowPassword ? <EyeInvisibleOutlined /> : <EyeOutlined />}
</span>
) : (
<></>
)}
</div>
);
};
@@ -323,7 +327,7 @@ const SecurityUsers = (): JSX.Element => {
};
const columns = () => {
const baseColumns = [
const baseColumns: any = [
{
title: 'KafkaUser',
dataIndex: 'name',
@@ -348,30 +352,39 @@ const SecurityUsers = (): JSX.Element => {
return <PasswordContent clusterId={clusterId} name={record.name} />;
},
},
{
];
if (global.hasPermission) {
baseColumns.push({
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>
{global.hasPermission(ClustersPermissionMap.SECURITY_USER_EDIT_PASSWORD) ? (
<Button
type="link"
size="small"
style={{ paddingLeft: 0 }}
onClick={() => editDrawerRef.current.onOpen(true, UsersOperate.ChangePassword, getKafkaUserList, record)}
>
</Button>
) : (
<></>
)}
{global.hasPermission(ClustersPermissionMap.SECURITY_USER_DELETE) ? (
<Button type="link" size="small" onClick={() => onDelete(record)}>
</Button>
) : (
<></>
)}
</>
);
},
},
];
});
}
return baseColumns;
};
@@ -454,13 +467,17 @@ const SecurityUsers = (): JSX.Element => {
setSearchKeywordsInput(e.target.value);
}}
/>
<Button
type="primary"
// icon={<PlusOutlined />}
onClick={() => editDrawerRef.current.onOpen(true, UsersOperate.Add, getKafkaUserList)}
>
KafkaUser
</Button>
{global.hasPermission && global.hasPermission(ClustersPermissionMap.SECURITY_USER_ADD) ? (
<Button
type="primary"
// icon={<PlusOutlined />}
onClick={() => editDrawerRef.current.onOpen(true, UsersOperate.Add, getKafkaUserList)}
>
KafkaUser
</Button>
) : (
<></>
)}
</div>
</div>

View File

@@ -8,6 +8,7 @@ import { useParams } from 'react-router-dom';
import TagsWithHide from '@src/components/TagsWithHide';
import SwitchTab from '@src/components/SwitchTab';
import RenderEmpty from '@src/components/RenderEmpty';
import { useForceRefresh } from '@src/components/utils';
interface PropsType {
hashData: any;
@@ -401,11 +402,18 @@ export default (props: PropsType) => {
const { hashData } = props;
const [showMode, setShowMode] = useState<string>('card');
const [refreshKey, forceRefresh] = useForceRefresh();
return (
<>
<div className="brokers-tab-container">
<div className="brokers-tab-container" key={`${refreshKey}`}>
<div className="overview">
<div className="left">
<span
style={{ display: 'inline-block', padding: '0 10px', marginRight: '10px', borderRight: '1px solid #ccc', fontSize: '15px' }}
onClick={forceRefresh as () => void}
>
<i className="iconfont icon-shuaxin1" style={{ cursor: 'pointer' }} />
</span>
<PartitionSummary clusterId={clusterId} topicName={hashData.topicName} />
</div>
<div className="cases-box">

View File

@@ -10,6 +10,7 @@ import { ClustersPermissionMap } from '../CommonConfig';
import ResetOffsetDrawer from './ResetOffsetDrawer';
import SwitchTab from '@src/components/SwitchTab';
import ContentWithCopy from '@src/components/CopyContent';
import PubSub from "pubsub-js";
const { Option } = Select;
@@ -335,6 +336,11 @@ export default (props: any) => {
});
}, [visible]);
// 订阅重置offset成功的消息
PubSub.subscribe('TopicDetail-ResetOffset', function(message, data){
getTopicGroupMetric({hashData: data});
})
useEffect(() => {
if (partitionList.length === 0) return;
getTopicGroupMetricHistory(partitionList, hashData);

View File

@@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom';
import EditTable from '../TestingProduce/component/EditTable';
import Api from '@src/api/index';
import moment from 'moment';
import PubSub from "pubsub-js";
const CustomSelectResetTime = (props: { value?: string; onChange?: (val: Number | String) => void }) => {
const { value, onChange } = props;
@@ -106,6 +107,13 @@ export default (props: any) => {
message: '重置offset成功',
});
setResetOffsetVisible(false);
// 发布重置offset成功的消息
PubSub.publish('TopicDetail-ResetOffset',
{
groupName: record.groupName,
topicName: record.topicName
}
);
} else {
notification.error({
message: '重置offset失败',

View File

@@ -81,7 +81,8 @@ export const getTopicMessagesColmns = () => {
title: 'Offset',
dataIndex: 'offset',
key: 'offset',
render: (t: number) => (t ? t.toLocaleString() : '-'),
sorter: true,
render: (t: number) => (+t || +t === 0 ? t.toLocaleString() : '-'), // TODO: 千分位展示
},
{
title: 'Timestamp',

View File

@@ -26,6 +26,7 @@
.left {
display: flex;
align-items: center;
.info-box {
display: flex;
height: 36px;

View File

@@ -15,9 +15,21 @@ import Replicator from './Replicator';
import './index.less';
import TopicDetailHealthCheck from '@src/components/CardBar/TopicDetailHealthCheck';
import { hashDataParse } from '@src/constants/common';
import { useForceRefresh } from '@src/components/utils';
const { TabPane } = Tabs;
const Reload = (props: any) => {
return (
<span
style={{ display: 'inline-block', padding: '0 10px', marginRight: '10px', borderRight: '1px solid #ccc', fontSize: '15px' }}
onClick={props.forceRefresh as () => void}
>
<i className="iconfont icon-shuaxin1" style={{ cursor: 'pointer' }} />
</span>
);
};
const OperationsSlot: any = {
// eslint-disable-next-line react/display-name
// ['Partitions']: (arg: any) => {
@@ -70,17 +82,20 @@ const OperationsSlot: any = {
// eslint-disable-next-line react/display-name
['ConsumerGroups']: (arg: any) => {
return (
<SearchInput
onSearch={arg.setSearchKeywords}
attrs={{
value: arg.searchValue,
onChange: arg.setSearchValue,
placeholder: '请输入Consumer Group',
size: 'small',
style: { width: '210px', marginRight: '2px' },
maxLength: 128,
}}
/>
<>
<Reload {...arg} />
<SearchInput
onSearch={arg.setSearchKeywords}
attrs={{
value: arg.searchValue,
onChange: arg.setSearchValue,
placeholder: '请输入Consumer Group',
size: 'small',
style: { width: '210px', marginRight: '2px' },
maxLength: 128,
}}
/>
</>
);
},
};
@@ -94,6 +109,7 @@ const TopicDetail = (props: any) => {
const [searchValue, setSearchValue] = useState<string>('');
const [visible, setVisible] = useState(false);
const [hashData, setHashData] = useState<any>({});
const [refreshKey, forceRefresh] = useForceRefresh();
const callback = (key: any) => {
setSearchValue('');
@@ -184,7 +200,7 @@ const TopicDetail = (props: any) => {
onChange={callback}
tabBarExtraContent={
OperationsSlot[positionType] &&
OperationsSlot[positionType]({ ...props, setSearchKeywords, setSearchValue, searchValue, positionType })
OperationsSlot[positionType]({ ...props, setSearchKeywords, setSearchValue, searchValue, positionType, forceRefresh })
}
destroyInactiveTabPane
>
@@ -196,7 +212,7 @@ const TopicDetail = (props: any) => {
</TabPane>
<TabPane tab="ConsumerGroups" key="ConsumerGroups">
{positionType === 'ConsumerGroups' && (
<Consumers searchKeywords={searchKeywords} positionType={positionType} hashData={hashData} />
<Consumers searchKeywords={searchKeywords} positionType={positionType} hashData={hashData} key={`${refreshKey}`} />
)}
</TabPane>
<TabPane tab="ACLs" key="ACLs">

View File

@@ -1,7 +1,22 @@
/* eslint-disable react/display-name */
import React, { useState, useEffect } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { AppContainer, Input, ProTable, Select, Switch, Tooltip, Utils, Dropdown, Menu, Button, Divider, Tag } from 'knowdesign';
import {
AppContainer,
Input,
ProTable,
Select,
Switch,
Tooltip,
Utils,
Dropdown,
Menu,
Button,
Divider,
Tag,
Popconfirm,
notification,
} from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import Create from './Create';
import './index.less';
@@ -85,6 +100,21 @@ const AutoPage = (props: any) => {
setTopicListLoading(false);
});
};
const deleteTopicData = (record: any) => {
console.log(record, 'record');
const params = {
clusterId: Number(routeParams.clusterId),
topicName: record.topicName,
};
Utils.post(Api.deleteTopicData(), params).then((data: any) => {
if (data === null) {
notification.success({
message: '清除数据成功',
});
getTopicsList();
}
});
};
useEffect(() => {
getTopicsList();
}, [sortObj, showInternalTopics, searchKeywords, pageIndex, pageSize]);
@@ -247,7 +277,7 @@ const AutoPage = (props: any) => {
dataIndex: 'desc',
key: 'desc',
fixed: 'right',
width: 140,
width: 200,
render: (value: any, record: any) => {
return (
<div className="operation-list">
@@ -257,6 +287,19 @@ const AutoPage = (props: any) => {
<></>
)}
{global.hasPermission(ClustersPermissionMap.TOPIC_DEL) ? <Delete record={record} onConfirm={getTopicsList}></Delete> : <></>}
{global.hasPermission(ClustersPermissionMap.TOPIC_DEL) ? ( // TODO替换为清除数据的权限
<Popconfirm
placement="topRight"
title={`是否要清空当前Topic的数据`}
onConfirm={() => deleteTopicData(record)}
okText="是"
cancelText="否"
>
<Button type="link"></Button>
</Popconfirm>
) : (
<></>
)}
</div>
);
},