This commit is contained in:
孙超
2022-12-15 18:48:41 +08:00
committed by EricZeng
parent 5bceed7105
commit 860d0b92e2
59 changed files with 25972 additions and 1497 deletions

View File

@@ -90,7 +90,7 @@ const ControllerChangeLogList: React.FC = (props: any) => {
<div style={{ margin: '12px 0' }}>
<BrokerHealthCheck />
</div>
<div className="clustom-table-content">
<div className="custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div

View File

@@ -98,7 +98,7 @@ const BrokerList: React.FC = (props: any) => {
<div style={{ margin: '12px 0' }}>
<BrokerHealthCheck />
</div>
<div className="clustom-table-content">
<div className="custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
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 React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ConnectCluster, ConnectorPlugin, ConnectorPluginConfig, OperateInfo } from './AddConnector';
const PLACEHOLDER = `配置格式如下
{
"connectClusterName": "", // Connect Cluster 名称
"configs": { // 具体配置项
"name": "",
"connector.class": "",
"tasks.max": 1,
...
}
}`;
export default forwardRef((props: any, ref) => {
const { clusterId } = useParams<{
clusterId: string;
}>();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
const [type, setType] = useState('create');
const [connectClusters, setConnectClusters] = useState<{ label: string; value: number }[]>([]);
const [defaultConfigs, setDefaultConfigs] = useState<{ [key: string]: any }>({});
const [submitLoading, setSubmitLoading] = useState(false);
const getConnectClusters = () => {
return Utils.request(api.getConnectClusters(clusterId)).then((res: ConnectCluster[]) => {
setConnectClusters(
res.map(({ name, id }) => ({
label: name || '-',
value: id,
}))
);
});
};
const onOpen = (type: 'create' | 'edit', connectClusterName?: string, defaultConfigs?: { [key: string]: any }) => {
if (defaultConfigs) {
setDefaultConfigs({ ...defaultConfigs, connectClusterName });
form.setFieldsValue({
configs: JSON.stringify(
{
connectClusterName,
configs: defaultConfigs,
},
null,
2
),
});
}
setType(type);
setVisible(true);
};
const onSubmit = () => {
setSubmitLoading(true);
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]) => {
if (val === null) {
delete postData.configs[key];
}
});
Utils.put(api.validateConnectorConfig, postData).then(
(res: ConnectorPluginConfig) => {
if (res) {
if (res?.errorCount > 0) {
const errors: OperateInfo['errors'] = {};
res?.configs
?.filter((config) => config.value.errors.length !== 0)
.forEach(({ value }) => {
if (value.name.includes('transforms.')) {
errors['transforms'] = (errors['transforms'] || []).concat(value.errors);
} else {
errors[value.name] = value.errors;
}
});
form.setFields([
{
name: 'configs',
errors: Object.entries(errors).map(([name, errorArr]) => `${name}: ${errorArr.join('; ')}\n`),
},
]);
setSubmitLoading(false);
} else {
if (type === 'create') {
Utils.post(api.connectorsOperates, postData)
.then(() => {
customMessage.success('新建成功');
onClose();
props?.refresh();
})
.finally(() => setSubmitLoading(false));
} else {
Utils.put(api.updateConnectorConfig, postData)
.then(() => {
customMessage.success('编辑成功');
props?.refresh();
onClose();
})
.finally(() => setSubmitLoading(false));
}
}
} else {
setSubmitLoading(false);
message.error('接口校验出错,请重新提交');
}
},
() => setSubmitLoading(false)
);
},
() => setSubmitLoading(false)
);
};
const onClose = () => {
setVisible(false);
form.resetFields();
};
useEffect(() => {
getConnectClusters();
}, []);
useImperativeHandle(ref, () => ({
onOpen,
onClose,
}));
return (
<Drawer
title={`${type === 'create' ? '新建' : '编辑'} Connector`}
className="operate-connector-drawer-use-json"
width={800}
visible={visible}
onClose={onClose}
maskClosable={false}
extra={
<div className="operate-wrap">
<Space>
<Button size="small" onClick={onClose}>
</Button>
<Button size="small" type="primary" onClick={onSubmit} loading={submitLoading}>
</Button>
<Divider type="vertical" />
</Space>
</div>
}
>
<Form form={form} layout="vertical">
<Form.Item
name="configs"
validateTrigger="onBlur"
rules={[
{
validator(rule, value) {
if (!value) {
return Promise.reject('配置不能为空');
}
try {
const v = JSON.parse(value);
if (typeof v !== 'object') {
return Promise.reject('输入内容必须为 JSON');
}
let connectClusterId = -1;
// 校验 connectClusterName 字段
if (!v.connectClusterName) {
return Promise.reject('内容缺少 connectClusterName 字段或字段内容为空');
} 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 (!v.configs || typeof v.configs !== 'object') {
return Promise.reject('内容缺少 configs 字段或字段格式错误');
} 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']) {
return Promise.reject('编辑模式下不允许修改 connector.class 字段');
}
}
if (type === 'create') {
// 异步校验 connector 名称是否重复 以及 className 是否存在
return Promise.all([
Utils.request(api.isConnectorExist(connectClusterId, v.configs.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'])
? Promise.reject('该 connectCluster 下不存在 connector.class 项配置的插件')
: Promise.resolve();
},
() => {
return Promise.reject('接口校验出错,请重试');
}
);
} else {
return Promise.resolve();
}
} catch (e) {
return Promise.reject('输入内容必须为 JSON');
}
},
},
]}
>
{visible && (
<div>
<CodeMirrorFormItem
resize
defaultInput={form.getFieldValue('configs')}
placeholder={PLACEHOLDER}
onBeforeChange={(configs: string) => {
form.setFieldsValue({ configs });
}}
/>
</div>
)}
</Form.Item>
</Form>
</Drawer>
);
});

View File

@@ -0,0 +1,99 @@
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 { IconFont } from '@knowdesign/icons';
import Api from '@src/api/index';
// eslint-disable-next-line react/display-name
const DeleteConnector = (props: { record: any; onConfirm?: () => void }) => {
const { record, onConfirm } = props;
const [form] = Form.useForm();
const [delDialogVisible, setDelDialogVisble] = useState(false);
const handleDelOk = () => {
form.validateFields().then((e) => {
const formVal = form.getFieldsValue();
formVal.connectClusterId = Number(record.connectClusterId);
Utils.delete(Api.connectorsOperates, { data: formVal }).then((res: any) => {
if (res === null) {
notification.success({
message: '删除成功',
});
setDelDialogVisble(false);
onConfirm && onConfirm();
} else {
notification.error({
message: '删除失败',
});
}
});
});
};
return (
<>
<Button
type="link"
size="small"
onClick={(_) => {
setDelDialogVisble(true);
}}
>
</Button>
<Modal
className="custom-modal"
title="确定删除此Connector吗"
centered={true}
visible={delDialogVisible}
wrapClassName="del-connect-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',
},
}}
>
<Form form={form} labelCol={{ span: 6 }} style={{ marginTop: 17 }}>
<Form.Item label="ConnectorName">{record.connectorName}</Form.Item>
<Form.Item
name="connectorName"
label="ConnectorName"
rules={[
// { required: true },
() => ({
validator(_, value) {
if (!value) {
return Promise.reject(new Error('请输入ConnectorName名称'));
} else if (value !== record.connectorName) {
return Promise.reject(new Error('请输入正确的ConnectorName名称'));
}
return Promise.resolve();
},
}),
]}
>
<Input placeholder="请输入" size="small"></Input>
</Form.Item>
</Form>
</Modal>
</>
);
};
export default DeleteConnector;

View File

@@ -0,0 +1,111 @@
import React, { useState, useEffect } from 'react';
import { Drawer, Utils, AppContainer, ProTable } from 'knowdesign';
import API from '@src/api';
import ConnectDetailCard from '@src/components/CardBar/ConnectDetailCard';
import { defaultPagination, getConnectorsDetailColumns } from './config';
import notification from '@src/components/Notification';
import './index.less';
const prefix = 'connect-detail';
const { request } = Utils;
const ConnectorDetail = (props: any) => {
const { visible, setVisible, record } = props;
const [global] = AppContainer.useGlobalValue();
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [pagination, setPagination] = useState<any>(defaultPagination);
const onClose = () => {
setVisible(false);
setPagination(defaultPagination);
// clean hash
};
// 请求接口获取数据
const genData = async () => {
if (global?.clusterInfo?.id === undefined) return;
setLoading(true);
request(API.getConnectDetailTasks(record.connectorName, record.connectClusterId))
.then((res: any) => {
setData(res || []);
setLoading(false);
})
.catch((err) => {
setLoading(false);
});
};
const onTableChange = (pagination: any, filters: any, sorter: any) => {
setPagination(pagination);
};
const optionFn: any = (taskId: any) => {
const params = {
action: 'restart',
connectClusterId: record?.connectClusterId,
connectorName: record?.connectorName,
taskId,
};
request(API.optionTasks(), { method: 'PUT', data: params }).then((res: any) => {
if (res === null) {
notification.success({
message: `任务重试成功`,
});
genData();
} else {
notification.error({
message: `任务重试失败`,
});
}
});
};
const retryOption = Utils.useDebounce(optionFn, 500);
useEffect(() => {
visible && record && genData();
}, [visible]);
return (
<Drawer
// push={false}
title={
<span>
<span style={{ fontSize: '18px', fontFamily: 'PingFangSC-Semibold', color: '#495057' }}>{record.connectorName ?? '-'}</span>
</span>
}
width={1080}
placement="right"
onClose={onClose}
visible={visible}
className={`${prefix}-drawer`}
destroyOnClose
maskClosable={false}
>
<ConnectDetailCard record={record} />
<div className={`${prefix}-drawer-title`}>Tasks</div>
<ProTable
key="connector-detail-table"
showQueryForm={false}
tableProps={{
showHeader: false,
rowKey: 'taskId',
loading: loading,
columns: getConnectorsDetailColumns({ retryOption }),
dataSource: data,
paginationProps: { ...pagination },
attrs: {
onChange: onTableChange,
// scroll: { x: 'max-content' },
bordered: false,
},
}}
/>
{/* <BrokerDetailHealthCheck record={{ brokerId: hashData?.brokerId }} /> */}
</Drawer>
);
};
export default ConnectorDetail;

View File

@@ -0,0 +1,51 @@
import React, { useLayoutEffect, useState } from 'react';
import api from '@src/api';
import { Spin, Utils } from 'knowdesign';
import { useParams } from 'react-router-dom';
import NodataImg from '@src/assets/no-data.png';
interface Props {
children: any;
}
const NoConnector = () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: 'calc(100vh - 118px)',
boxShadow: '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)',
borderRadius: 12,
background: '#fff',
}}
>
<img src={NodataImg} style={{ width: 100, height: 162 }} />
<span style={{ fontSize: 13, color: '#919AAC', paddingTop: 16 }}> Connect </span>
</div>
);
};
export default (props: Props) => {
const { clusterId } = useParams<{
clusterId: string;
}>();
const [loading, setLoading] = useState(true);
const [disabled, setDisabled] = useState(true);
useLayoutEffect(() => {
Utils.request(api.getConnectors(clusterId))
.then((res: any[]) => {
res?.length && setDisabled(false);
})
.finally(() => setLoading(false));
}, []);
return disabled ? (
<Spin spinning={loading}>{loading ? <div style={{ height: 'calc(100vh - 118px)' }} /> : <NoConnector />}</Spin>
) : (
props.children
);
};

View File

@@ -0,0 +1,120 @@
import React, { useState, useEffect, memo } from 'react';
import { useParams, useHistory, useLocation } from 'react-router-dom';
import { ProTable, Button, Utils, AppContainer, SearchInput } from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import API from '../../api';
import { getWorkersColumns, defaultPagination } from './config';
import { tableHeaderPrefix } from '@src/constants/common';
import ConnectCard from '@src/components/CardBar/ConnectCard';
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
import './index.less';
import HasConnector from './HasConnector';
const { request } = Utils;
const Workers: React.FC = () => {
const [global] = AppContainer.useGlobalValue();
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [searchKeywords, setSearchKeywords] = useState('');
const [pagination, setPagination] = useState<any>(defaultPagination);
// 请求接口获取数据
const genData = async ({ pageNo, pageSize, filters, sorter }: any) => {
if (global?.clusterInfo?.id === undefined) return;
setLoading(true);
const params = {
searchKeywords: searchKeywords.slice(0, 128),
pageNo,
pageSize,
};
request(API.getWorkersList(global?.clusterInfo?.id), { params })
.then((res: any) => {
setPagination({
current: res.pagination?.pageNo,
pageSize: res.pagination?.pageSize,
total: res.pagination?.total,
});
setData(res?.bizData || []);
setLoading(false);
})
.catch((err) => {
setLoading(false);
});
};
const onTableChange = (pagination: any, filters: any, sorter: any) => {
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
};
useEffect(() => {
genData({
pageNo: 1,
pageSize: pagination.pageSize,
});
}, [searchKeywords]);
return (
<>
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
<DBreadcrumb
breadcrumbs={[
{ label: '多集群管理', aHref: '/' },
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
{ label: 'Connect', aHref: `/cluster/${global?.clusterInfo?.id}/connect` },
{ label: 'Workers', aHref: `` },
]}
/>
</div>
<HasConnector>
<>
<div style={{ margin: '12px 0' }}>
<ConnectCard />
</div>
<div className="custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div
className={`${tableHeaderPrefix}-left-refresh`}
onClick={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize })}
>
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
</div>
</div>
<div className={`${tableHeaderPrefix}-right`}>
<SearchInput
onSearch={setSearchKeywords}
attrs={{
placeholder: '请输入Host',
style: { width: '248px', borderRiadus: '8px' },
maxLength: 128,
}}
/>
</div>
</div>
<ProTable
key="workers-table"
showQueryForm={false}
tableProps={{
showHeader: false,
rowKey: 'workers_list',
loading: loading,
columns: getWorkersColumns(),
dataSource: data,
paginationProps: { ...pagination },
attrs: {
onChange: onTableChange,
scroll: { y: 'calc(100vh - 400px)' },
bordered: false,
},
}}
/>
</div>
</>
</HasConnector>
</>
);
};
export default Workers;

View File

@@ -0,0 +1,364 @@
import SmallChart from '@src/components/SmallChart';
import TagsWithHide from '@src/components/TagsWithHide';
import { Button, Tag, Tooltip, Utils, Popconfirm } from 'knowdesign';
import React from 'react';
import Delete from './Delete';
export const defaultPagination = {
current: 1,
pageSize: 10,
position: 'bottomRight',
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
};
export const optionType: { [name: string]: string } = {
['stop']: '暂停',
['restart']: '重启',
['resume']: '继续',
};
export const stateEnum: any = {
['UNASSIGNED']: {
// 未分配
name: 'Unassigned',
color: '#556EE6',
bgColor: '#EBEEFA',
},
['RUNNING']: {
// 运行
name: 'Running',
color: '#00C0A2',
bgColor: 'rgba(0,192,162,0.10)',
},
['PAUSED']: {
// 暂停
name: 'Paused',
color: '#495057',
bgColor: '#ECECF6',
},
['FAILED']: {
// 失败
name: 'Failed',
color: '#F58342',
bgColor: '#fef3e5',
},
['DESTROYED']: {
// 销毁
name: 'Destroyed',
color: '#FF7066',
bgColor: '#fdefee',
},
['RESTARTING']: {
// 重新启动
name: 'Restarting',
color: '#3991FF',
bgColor: '#e9f5ff',
},
};
const calcCurValue = (record: any, metricName: string) => {
// const item = (record.metricPoints || []).find((item: any) => item.metricName === metricName);
// return item?.value || '';
// TODO 替换record
const orgVal = record?.latestMetrics?.metrics?.[metricName];
if (orgVal !== undefined) {
if (metricName === 'TotalRecordErrors') {
return Math.round(orgVal).toLocaleString();
} else {
return Number(Utils.formatAssignSize(orgVal, 'KB', orgVal > 1000 ? 2 : 3)).toLocaleString();
// return Utils.formatAssignSize(orgVal, 'KB');
}
}
return '-';
// return orgVal !== undefined ? (metricName !== 'HealthScore' ? formatAssignSize(orgVal, 'KB') : orgVal) : '-';
};
const renderLine = (record: any, metricName: string) => {
const points = record.metricLines?.find((item: any) => item.metricName === metricName)?.metricPoints || [];
return points.length ? (
<div className="metric-data-wrap">
<SmallChart
width={'100%'}
height={30}
chartData={{
name: record.metricName,
data: points.map((item: any) => ({ time: item.timeStamp, value: item.value })),
}}
/>
<span className="cur-val">{calcCurValue(record, metricName)}</span>
</div>
) : (
<span className="cur-val">{calcCurValue(record, metricName)}</span>
);
};
export const getConnectorsColumns = (arg?: any) => {
const columns = [
{
title: 'Connect集群',
dataIndex: 'connectClusterName',
key: 'connectClusterName',
width: 200,
fixed: 'left',
lineClampOne: true,
needTooltip: true,
// render: (t: string, r: any) => {
// return (
// <span>
// {t}
// {r?.status ? <Tag className="tag-success">Live</Tag> : <Tag className="tag-error">Down</Tag>}
// </span>
// );
// },
},
{
title: 'Connector Name',
dataIndex: 'connectorName',
key: 'connectorName',
width: 160,
lineClampOne: true,
render: (t: string, r: any) => {
return t ? (
<>
<Tooltip placement="bottom" title={t}>
<a
onClick={() => {
arg.getDetailInfo(r);
}}
>
{t}
</a>
</Tooltip>
</>
) : (
'-'
);
},
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
width: 120,
render: (t: string, r: any) => {
return t ? (
<Tag
style={{
background: stateEnum[t]?.bgColor,
color: stateEnum[t]?.color,
padding: '3px 6px',
}}
>
{stateEnum[t]?.name}
</Tag>
) : (
'-'
);
},
},
{
title: 'Class',
dataIndex: 'connectorClassName',
key: 'connectorClassName',
width: 150,
lineClampOne: true,
needTooltip: true,
},
{
title: 'Type',
dataIndex: 'connectorType',
key: 'connectorType',
width: 100,
render: (value: any, record: any) => Utils.firstCharUppercase(value),
},
{
title: 'Tasks',
dataIndex: 'taskCount',
key: 'taskCount',
width: 100,
},
{
title: '消息读取速率(KB/s)',
dataIndex: 'readRate',
key: 'readRate',
sorter: true,
width: 170,
render: (value: any, record: any) =>
renderLine(record, record.connectorType === 'SINK' ? 'SinkRecordReadRate' : 'SourceRecordPollRate'),
},
{
title: '消息写入速率(KB/s)',
dataIndex: 'writeRate',
key: 'writeRate',
sorter: true,
width: 170,
render: (value: any, record: any) =>
renderLine(record, record.connectorType === 'SINK' ? 'SinkRecordSendRate' : 'SourceRecordWriteRate'),
},
{
title: '消息处理错误次数(次)',
dataIndex: 'recordErrors',
key: 'recordErrors',
sorter: true,
width: 170,
render: (value: any, record: any) => renderLine(record, 'TotalRecordErrors'),
},
{
title: 'Topics',
dataIndex: 'topicNameList',
key: 'topicNameList',
width: 200,
render(t: any, r: any) {
return t && t.length > 0 ? <TagsWithHide placement="bottom" list={t} expandTagContent={(num: any) => `共有${num}`} /> : '-';
},
},
{
title: '操作',
dataIndex: 'options',
key: 'options',
width: 200,
filterTitle: true,
fixed: 'right',
// eslint-disable-next-line react/display-name
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>
{(r.state === 'RUNNING' || r.state === 'PAUSED') && (
<Popconfirm
title={`是否${r.state === 'RUNNING' ? '暂停' : '继续'}当前任务?`}
onConfirm={() => arg?.optionConnect(r, r.state === 'RUNNING' ? 'stop' : 'resume')}
// onCancel={cancel}
okText="是"
cancelText="否"
overlayClassName="connect-popconfirm"
>
<Button key="stopResume" type="link" size="small">
{/* {r?.state !== 1 ? '继续' : '暂停'} */}
{r.state === 'RUNNING' ? '暂停' : '继续'}
</Button>
</Popconfirm>
)}
<Button type="link" size="small" onClick={() => arg?.editConnector(r)}>
</Button>
<Delete record={r} onConfirm={arg?.deleteTesk}></Delete>
</div>
);
},
},
];
return columns;
};
// Workers
export const getWorkersColumns = (arg?: any) => {
const columns = [
{
title: 'Worker Host',
dataIndex: 'workerHost',
key: 'workerHost',
width: 200,
},
{
title: '所属集群',
dataIndex: 'connectClusterName',
key: 'connectClusterName',
width: 200,
},
{
title: 'Connectors',
dataIndex: 'connectorCount',
key: 'connectorCount',
width: 200,
},
{
title: 'Tasks',
dataIndex: 'taskCount',
key: 'taskCount',
width: 200,
},
];
return columns;
};
// Detail
export const getConnectorsDetailColumns = (arg?: any) => {
const columns = [
{
title: 'Task ID',
dataIndex: 'taskId',
key: 'taskId',
width: 240,
render: (t: any, r: any) => {
return (
<span>
{t}
{
<Tag
style={{
background: stateEnum[r?.state]?.bgColor,
color: stateEnum[r?.state]?.color,
padding: '3px 6px',
marginLeft: '5px',
}}
>
{Utils.firstCharUppercase(r?.state as string)}
</Tag>
}
</span>
);
},
},
{
title: 'Worker',
dataIndex: 'workerId',
key: 'workerId',
width: 240,
},
{
title: '错误原因',
dataIndex: 'trace',
key: 'trace',
width: 400,
needTooltip: true,
lineClampOne: true,
},
{
title: '操作',
dataIndex: 'role',
key: 'role',
width: 100,
render: (_t: any, r: any) => {
return (
<div>
<Popconfirm
title="是否重试当前任务?"
onConfirm={() => arg?.retryOption(r.taskId)}
// onCancel={cancel}
okText="是"
cancelText="否"
overlayClassName="connect-popconfirm"
>
<a></a>
</Popconfirm>
</div>
);
},
},
];
return columns;
};

View File

@@ -0,0 +1,193 @@
// connect列表 图表
.metric-data-wrap {
// display: flex;
// align-items: center;
width: 100%;
.cur-val {
display: block;
text-align: right;
}
.dcloud-spin-nested-loading {
flex: 1;
}
}
// 新增按钮
.add-connect {
.dcloud-btn-primary:hover,
.dcloud-btn-primary:focus {
// 可以控制新增按钮的hover和focus的样式
// background: #556ee6;
// border-color: #556ee6;
}
&-btn {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
border-right: none;
}
&-dropdown-menu {
.dcloud-dropdown-menu {
border-radius: 8px;
&-item {
font-size: 13px;
}
}
}
&-json {
padding: 8px 12px !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-left: none;
}
}
// connect详情
.connect-detail-drawer {
.card-bar-container {
background: rgba(86, 110, 230, 0.04) !important;
.card-bar-colunms {
background-color: rgba(86, 110, 230, 0);
}
}
&-title {
margin: 20px 0 8px;
font-family: @font-family-bold;
font-size: 16px;
font-weight: 500;
}
}
// 删除
.del-connect-modal {
.tip-info {
display: flex;
color: #592d00;
padding: 6px 14px;
font-size: 13px;
background: #fffae0;
border-radius: 4px;
.anticon {
color: #ffc300;
margin-right: 4px;
margin-top: 3px;
}
.test-right-away {
color: #556ee6;
cursor: pointer;
}
.dcloud-alert-content {
flex: none;
}
}
}
// 重启、继续/暂停 气泡卡片
.connect-popconfirm {
.dcloud-popover-inner-content {
padding: 6px 16px;
}
.dcloud-popover-inner {
border-radius: 8px;
}
.dcloud-popover-message,
.dcloud-btn {
font-size: 12px !important;
}
}
.operate-connector-drawer {
.connector-plugin-desc {
font-size: 13px;
.connector-plugin-title {
font-family: @font-family-bold;
}
}
.dcloud-collapse.add-connector-collapse {
.add-connector-collapse-panel,
.add-connector-collapse-panel:last-child {
margin-bottom: 8px;
overflow: hidden;
background: #f8f9fa;
border: 0px;
border-radius: 8px;
.dcloud-collapse-header {
padding: 8px 12px;
font-size: 14px;
color: #495057;
.dcloud-collapse-arrow {
margin-right: 8px !important;
font-size: 10px;
}
}
&:hover .dcloud-collapse-extra {
opacity: 1;
}
&:not(.dcloud-collapse-item-active) {
.dcloud-collapse-header:hover {
background: #f1f3ff;
}
}
.dcloud-collapse-content-box {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
padding: 20px 14px 0 14px;
.dcloud-form-item {
flex: 1 0 50%;
.dcloud-input-number {
width: 100%;
}
&:nth-child(2n) {
padding-left: 6px;
}
&:nth-child(2n + 1) {
padding-right: 6px;
}
.dcloud-form-item-control-input {
height: 100%;
}
}
}
}
}
.add-container-plugin-select {
height: 27px;
margin: 4px 0;
position: relative;
.dcloud-form-item {
position: absolute;
top: 0;
left: 0;
.dcloud-form-item-control-input {
min-height: 0;
}
}
}
.dcloud-alert {
margin: 16px 0 24px 0;
padding: 0 12px;
border: unset;
border-radius: 8px;
background: #fffae0;
.dcloud-alert-message {
font-size: 13px;
color: #592d00;
.dcloud-btn {
padding: 0 4px;
font-size: 13px;
}
}
}
}
.operate-connector-drawer-use-json {
.CodeMirror.cm-s-default {
height: calc(100vh - 146px);
}
.dcloud-form-item {
margin-bottom: 0 !important;
}
}

View File

@@ -0,0 +1,228 @@
import React, { useState, useEffect, useRef } from 'react';
import { ProTable, Dropdown, Button, Utils, AppContainer, SearchInput, Menu } from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import API from '../../api';
import { getConnectorsColumns, defaultPagination, optionType } from './config';
import { tableHeaderPrefix } from '@src/constants/common';
import ConnectCard from '@src/components/CardBar/ConnectCard';
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
import AddConnector, { OperateInfo } from './AddConnector';
import ConnectorDetail from './Detail';
import notification from '@src/components/Notification';
import './index.less';
import AddConnectorUseJSON from './AddConnectorUseJSON';
import HasConnector from './HasConnector';
const { request } = Utils;
const rateMap: any = {
readRate: ['SinkRecordReadRate', 'SourceRecordPollRate'],
writeRate: ['SinkRecordSendRate', 'SourceRecordWriteRate'],
recordErrors: ['TotalRecordErrors'],
};
const Connectors: React.FC = () => {
const [global] = AppContainer.useGlobalValue();
const [loading, setLoading] = useState(false);
const [detailVisible, setDetailVisible] = useState(false);
const [data, setData] = useState([]);
const [searchKeywords, setSearchKeywords] = useState('');
const [pagination, setPagination] = useState<any>(defaultPagination);
const [sortInfo, setSortInfo] = useState({});
const [detailRecord, setDetailRecord] = useState('');
const [healthType, setHealthType] = useState(true);
const addConnectorRef = useRef(null);
const addConnectorJsonRef = useRef(null);
const getRecent1DayTimeStamp = () => [Date.now() - 24 * 60 * 60 * 1000, Date.now()];
// 请求接口获取数据
const genData = async ({ pageNo, pageSize, filters, sorter }: any) => {
const [startStamp, endStamp] = getRecent1DayTimeStamp();
if (global?.clusterInfo?.id === undefined) return;
setLoading(true);
const params = {
metricLines: {
aggType: 'avg',
endTime: endStamp,
metricsNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
startTime: startStamp,
topNu: 0,
},
searchKeywords: searchKeywords.slice(0, 128),
pageNo,
pageSize,
latestMetricNames: ['SourceRecordPollRate', 'SourceRecordWriteRate', 'SinkRecordReadRate', 'SinkRecordSendRate', 'TotalRecordErrors'],
sortType: sorter?.order ? sorter.order.substring(0, sorter.order.indexOf('end')) : 'desc',
sortMetricNameList: rateMap[sorter?.field] || [],
};
request(API.getConnectorsList(global?.clusterInfo?.id), { method: 'POST', data: params })
.then((res: any) => {
setPagination({
current: res.pagination?.pageNo,
pageSize: res.pagination?.pageSize,
total: res.pagination?.total,
});
const newData =
res?.bizData.map((item: any) => {
return {
...item,
...item?.latestMetrics?.metrics,
key: item.connectClusterName + item.connectorName,
};
}) || [];
setData(newData);
setLoading(false);
})
.catch((err) => {
setLoading(false);
});
};
const onTableChange = (pagination: any, filters: any, sorter: any) => {
setSortInfo(sorter);
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, filters, sorter });
};
const menu = (
<Menu className="">
<Menu.Item>
<span onClick={() => addConnectorJsonRef.current?.onOpen('create')}>JSON Connector</span>
</Menu.Item>
</Menu>
);
const getDetailInfo = (record: any) => {
setDetailRecord(record);
setDetailVisible(true);
};
// 编辑
const editConnector = (detail: OperateInfo['detail']) => {
addConnectorRef.current?.onOpen('edit', addConnectorJsonRef.current, detail);
};
// 重启、暂停/继续 操作
const optionConnect = (record: any, action: string) => {
const params = {
action,
connectClusterId: record?.connectClusterId,
connectorName: record?.connectorName,
};
request(API.connectorsOperates, { method: 'PUT', data: params }).then((res: any) => {
if (res === null) {
notification.success({
message: `任务已${optionType[params.action]}`,
description: `任务状态更新会有至多1min延迟`,
});
genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo });
setHealthType(!healthType);
} else {
notification.error({
message: `${optionType[params.action]}任务失败`,
});
}
});
};
// 删除任务
const deleteTesk = () => {
genData({ pageNo: 1, pageSize: pagination.pageSize });
};
useEffect(() => {
genData({
pageNo: 1,
pageSize: pagination.pageSize,
sorter: sortInfo,
});
}, [searchKeywords]);
return (
<>
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
<DBreadcrumb
breadcrumbs={[
{ label: '多集群管理', aHref: '/' },
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
{ label: 'Connect', aHref: `/cluster/${global?.clusterInfo?.id}/connect` },
{ label: 'Connectors', aHref: `` },
]}
/>
</div>
<HasConnector>
<>
<div style={{ margin: '12px 0' }}>
<ConnectCard state={healthType} />
</div>
<div className="custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div
className={`${tableHeaderPrefix}-left-refresh`}
onClick={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize })}
>
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
</div>
</div>
<div className={`${tableHeaderPrefix}-right`}>
<SearchInput
onSearch={setSearchKeywords}
attrs={{
placeholder: '请输入Connector',
style: { width: '248px', borderRiadus: '8px' },
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" />
</Button>
</Dropdown>
</span>
</div>
</div>
<ProTable
key="connector-table"
showQueryForm={false}
tableProps={{
showHeader: false,
rowKey: 'key',
loading: loading,
columns: getConnectorsColumns({ getDetailInfo, deleteTesk, optionConnect, editConnector }),
dataSource: data,
paginationProps: { ...pagination },
attrs: {
onChange: onTableChange,
scroll: { x: 'max-content', y: 'calc(100vh - 400px)' },
bordered: false,
},
}}
/>
</div>
</>
</HasConnector>
<ConnectorDetail visible={detailVisible} setVisible={setDetailVisible} record={detailRecord} />
<AddConnector
ref={addConnectorRef}
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
/>
<AddConnectorUseJSON
ref={addConnectorJsonRef}
refresh={() => genData({ pageNo: pagination.current, pageSize: pagination.pageSize, sorter: sortInfo })}
/>
</>
);
};
export default Connectors;

View File

@@ -0,0 +1,298 @@
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react';
import { Drawer, Button, Space, Divider, ProTable } from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import { ITableColumnsType } from 'knowdesign/lib/extend/d-table';
import { cloneDeep } from 'lodash';
import { MetricType } from '@src/api';
import '../../components/ChartOperateBar/style/indicator-drawer.less';
export interface TreeTableDataSourceType {
set?: boolean;
children?: TreeTableData;
[key: string]: any;
}
export interface TreeTableData {
showHeader: boolean;
rowKey: string;
columns: ITableColumnsType[];
dataSource: TreeTableDataSourceType[];
}
export interface CheckboxStatus {
type: MetricType;
key: string;
status: boolean | 'indeterminate';
children?: CheckboxStatus[];
}
export interface TreeTableProps {
tree: TreeTableData;
checkField: string;
submitCallback: ([leafs, checkboxData]: [any[], CheckboxStatus]) => Promise<any>;
}
const TreeTableItem = (props: {
treeData: TreeTableData;
checkboxData: CheckboxStatus[];
keyTrace: (string | number)[];
onKeyChange: any;
}) => {
const { treeData, checkboxData: data, keyTrace, onKeyChange } = props;
return (
<ProTable
tableProps={{
noPagination: true,
showHeader: false,
rowKey: treeData.rowKey,
columns: treeData.columns,
dataSource: treeData.dataSource,
attrs: Object.assign(
{
showHeader: treeData.showHeader,
rowSelection: {
hideSelectAll: keyTrace.length !== 0,
selectedRowKeys: data.filter((item) => item.status === true).map((item) => item.key),
onChange: (keys: string[]) => {
onKeyChange(keyTrace, keys);
},
getCheckboxProps: (record: TreeTableDataSourceType) => {
return {
indeterminate: data.some((item) => item.key === record[treeData.rowKey] && item.status === 'indeterminate'),
};
},
},
},
treeData.dataSource.some((item) => item.children)
? {
expandable: {
childrenColumnName: 'notExist',
expandRowByClick: true,
expandedRowRender: (record: TreeTableDataSourceType) => {
return record?.children ? (
<TreeTableItem
keyTrace={[...keyTrace, record[treeData.rowKey]]}
treeData={record.children}
checkboxData={data.find((item) => record[treeData.rowKey] === item.key)?.children}
onKeyChange={onKeyChange}
/>
) : (
<></>
);
},
rowExpandable: (record: TreeTableDataSourceType) => {
return !!record?.children;
},
expandIcon: ({ expanded, onExpand, record }: { expanded: boolean; onExpand: any; record: TreeTableDataSourceType }) => {
return record?.children ? (
expanded ? (
<IconFont
style={{ fontSize: '16px' }}
type="icon-xia"
onClick={(e: any) => {
onExpand(record, e);
}}
/>
) : (
<IconFont
style={{ fontSize: '16px' }}
type="icon-jiantou_1"
onClick={(e: any) => {
onExpand(record, e);
}}
/>
)
) : (
<></>
);
},
},
}
: {}
),
}}
/>
);
};
export const TreeTable = forwardRef((props: TreeTableProps, ref) => {
const [treeData, setTreeData] = useState(props.tree);
const [checkboxData, setCheckboxData] = useState<CheckboxStatus>();
// 根据链条查找指定选择框
const findTargetCheckbox = (checkboxData: CheckboxStatus, keyTrace: (string | number)[]) => {
if (keyTrace.length === 0) {
return checkboxData;
}
let targetCheckbox: CheckboxStatus = checkboxData;
keyTrace.forEach((key) => {
Object.values(targetCheckbox?.children).some((checkbox) => {
if (key === checkbox.key) {
targetCheckbox = checkbox;
return true;
} else {
return false;
}
});
});
return targetCheckbox;
};
const changeStatus = (checkbox: CheckboxStatus, type: CheckboxStatus['status']) => {
checkbox.status = type;
(checkbox?.children || []).forEach((c) => changeStatus(c, type));
};
// 更新节点状态
const updateCheckStatus = (data: CheckboxStatus, keyTrace: (string | number)[], keys: string[]) => {
// 1. 设置更新元素及其子元素的状态
const targetCheckbox = findTargetCheckbox(data, keyTrace);
(targetCheckbox?.children || []).forEach((checkbox) => {
if (keys.includes(checkbox.key)) {
changeStatus(checkbox, true);
} else if (checkbox.status === true) {
changeStatus(checkbox, false);
}
});
// 2. 更新自身状态
targetCheckbox.status = keys?.length ? (keys.length === targetCheckbox?.children?.length ? true : 'indeterminate') : false;
// 3. 更新父级选中状态
for (let i = 1; i < keyTrace.length; i++) {
const newTrace = keyTrace.slice(0, keyTrace.length - i);
const newCheckbox = findTargetCheckbox(data, newTrace);
const trueLen = newCheckbox?.children.map((item) => item.status === true).filter((s) => s).length;
newCheckbox.status = trueLen === 0 ? false : trueLen === newCheckbox?.children.length ? true : 'indeterminate';
}
};
const onKeyChange = (keyTrace: (string | number)[], keys: string[]) => {
const clonedCheckboxData = cloneDeep(checkboxData);
updateCheckStatus(clonedCheckboxData, keyTrace, keys);
setCheckboxData(clonedCheckboxData);
};
// 生成初始选中结构
const renderInitCheckboxData = (
tree: TreeTableData,
curCheckboxData: CheckboxStatus,
wholeCheckboxData: CheckboxStatus,
keyTrace: (string | number)[],
callbacks: (() => void)[]
) => {
curCheckboxData.children = [];
const selectedKeys: string[] = [];
tree.dataSource.forEach((item) => {
const data = {
type: item.type,
key: item[tree.rowKey],
status: false,
};
curCheckboxData.children.push(data);
if (item.children) {
renderInitCheckboxData(item.children, data, wholeCheckboxData, [...keyTrace, item[tree.rowKey]], callbacks);
} else if (item.set) {
selectedKeys.push(item[tree.rowKey]);
}
});
if (selectedKeys.length) {
callbacks.push(() => updateCheckStatus(wholeCheckboxData, keyTrace, selectedKeys));
}
};
// 获取叶子节点状态数组
const getLeafNodes = (checkboxData: CheckboxStatus, arr: any[]) => {
checkboxData?.children.forEach((item) => {
if (item?.children) {
getLeafNodes(item, arr);
} else {
arr.push({ ...item });
}
});
};
const getLeafCheckboxData = () => {
const leafs: any[] = [];
getLeafNodes(checkboxData, leafs);
return [leafs, checkboxData];
};
// 初始化选中状态数据结构
useEffect(() => {
const initCheckbox: CheckboxStatus = {
type: undefined,
key: undefined,
status: false,
};
const callbacks: (() => void)[] = [];
renderInitCheckboxData(treeData, initCheckbox, initCheckbox, [], callbacks);
callbacks.forEach((cb) => cb());
setCheckboxData(initCheckbox);
}, [treeData]);
useEffect(() => {
setTreeData(props.tree);
}, [props.tree]);
useImperativeHandle(ref, () => ({
getLeafCheckboxData,
}));
return checkboxData ? (
<TreeTableItem treeData={treeData} checkboxData={checkboxData?.children} keyTrace={[]} onKeyChange={onKeyChange} />
) : (
<></>
);
});
export const TreeTableDrawer = forwardRef((props: TreeTableProps, ref) => {
const [visible, setVisible] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const treeTableRef = useRef(null);
const onSubmit = () => {
setConfirmLoading(true);
props
.submitCallback(treeTableRef.current?.getLeafCheckboxData())
.then(() => {
setVisible(false);
})
.finally(() => {
setConfirmLoading(false);
});
};
useImperativeHandle(ref, () => ({
open: () => setVisible(true),
}));
return (
<Drawer
className="indicator-drawer"
title="指标筛选"
width="868px"
forceRender={true}
onClose={() => setVisible(false)}
visible={visible}
maskClosable={false}
destroyOnClose
extra={
<Space>
<Button size="small" onClick={() => setVisible(false)}>
</Button>
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
</Button>
<Divider type="vertical" />
</Space>
}
>
<TreeTable ref={treeTableRef} {...props} />
</Drawer>
);
});

View File

@@ -0,0 +1,235 @@
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
import api, { MetricType } from '@src/api';
import { arrayMoveImmutable } from 'array-move';
import { expandedRowColumns } from '@src/components/ChartOperateBar/MetricSelect';
import { TreeTableDataSourceType, TreeTableDrawer, TreeTableData, CheckboxStatus } from './MetricSelect';
import { MetricInfo } from '@src/constants/chartConfig';
import { forwardRef } from 'react';
import { AppContainer, Utils } from 'knowdesign';
import { useParams } from 'react-router-dom';
interface MetricsFilterProps {
metricType: MetricType[];
onSelectChange: (list: MetricInfo[]) => void;
onRankChange: (rankList: string[]) => void;
}
// 处理图表排序
const resolveMetricsRank = (metricList: MetricInfo[]) => {
const isRanked = metricList.some(({ rank }) => rank !== null);
const listInfo: { [key: string]: { metric: string; rank: number; set: boolean }[] } = {};
let shouldUpdate = false;
let sortedList: MetricInfo[] = [];
if (isRanked) {
const rankedMetrics = metricList.filter(({ rank }) => rank !== null).sort((a, b) => a.rank - b.rank);
const unRankedMetrics = metricList.filter(({ rank }) => rank === null);
// 如果有新增/删除指标的情况,需要触发更新
if (unRankedMetrics.length || rankedMetrics.some(({ rank }, i) => rank !== i)) {
shouldUpdate = true;
}
sortedList = [...rankedMetrics, ...unRankedMetrics.sort((a, b) => Number(a.name > b.name) - 0.5)];
} else {
shouldUpdate = true;
// 按字母先后顺序初始化指标排序
sortedList = metricList.sort((a, b) => a.type - b.type).sort((a, b) => (a.type !== b.type ? 1 : Number(a.name > b.name) - 0.5));
}
sortedList.forEach((metric, rank) => {
!listInfo[metric.type] && (listInfo[metric.type] = []);
listInfo[metric.type].push({ metric: metric.name, rank, set: metricList.find(({ name }) => metric.name === name)?.set || false });
});
return {
list: sortedList.map(({ name }) => name),
listInfo,
shouldUpdate,
};
};
const MetricsFilter = forwardRef((props: MetricsFilterProps, ref) => {
const [global] = AppContainer.useGlobalValue();
const { metricType, onSelectChange, onRankChange } = props;
const { clusterId } = useParams<{
clusterId: string;
}>();
const [metricsList, setMetricsList] = useState<MetricInfo[]>([]); // 指标列表
const [metricRankList, setMetricRankList] = useState<string[]>([]);
const [tree, setTree] = useState<TreeTableData>();
const metricSelectRef = useRef(null);
// 更新指标
const setMetricList = (list: { [key: string]: { metric: string; rank: number; set: boolean }[] }) => {
const reqArr: Promise<any>[] = [];
Object.entries(list).forEach(([type, metricDetailDTOList]) => {
reqArr.push(
Utils.request(api.getDashboardMetricList(clusterId, type as unknown as MetricType), {
method: 'POST',
data: {
metricDetailDTOList,
},
})
);
});
return Promise.all(reqArr);
};
// TODO: 图表展示顺序变更
const rankChange = (oldIndex: number, newIndex: number) => {
const newList = arrayMoveImmutable(metricRankList, oldIndex, newIndex);
setMetricRankList(newList);
const updates: { [key: string]: { metric: string; rank: number; set: boolean }[] } = {};
newList.forEach((metric, rank) => {
const targetMetric = metricsList.find(({ name }) => metric === name);
if (targetMetric) {
const info = { metric, rank, set: targetMetric?.set || false };
updates[targetMetric.type] ? updates[targetMetric.type].push(info) : (updates[targetMetric.type] = [info]);
}
});
setMetricList(updates);
};
// 更新 rank
const updateRank = (metricList: MetricInfo[]) => {
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
setMetricRankList(list);
if (shouldUpdate) {
setMetricList(listInfo);
}
};
// 获取指标列表
const getMetricList = () => {
Promise.all(metricType.map((type) => Utils.request(api.getDashboardMetricList(clusterId, type)))).then((list) => {
let allSupportMetrics: MetricInfo[] = [];
const treeData: TreeTableData = {
showHeader: true,
rowKey: 'category',
columns: [{ title: '分类', dataIndex: 'category' }],
dataSource: [],
};
(list as unknown as (MetricInfo[] | null)[]).forEach((res, i) => {
if (!res) return;
const supportMetrics = res.filter((metric) => metric.support);
allSupportMetrics = allSupportMetrics.concat(supportMetrics);
// 处理结构
const data: TreeTableDataSourceType = {
category: metricType[i] === MetricType.Connect ? 'Connect Cluster' : 'Connector',
};
const categoryData: {
[category: string]: {
name: string;
unit: string;
desc: string;
}[];
} = {};
supportMetrics.forEach(({ name, desc, set }) => {
const metricDefine = global.getMetricDefine(metricType[i], name);
const returnData = {
type: metricType[i],
set,
name,
desc,
unit: metricDefine?.unit,
};
if (metricDefine.category) {
if (!categoryData[metricDefine.category]) {
categoryData[metricDefine.category] = [returnData];
} else {
categoryData[metricDefine.category].push(returnData);
}
} else {
if (!categoryData['Other']) {
categoryData['Other'] = [returnData];
} else {
categoryData['Other'].push(returnData);
}
}
});
if (Object.keys(categoryData).length > 1) {
const returnData: TreeTableData = {
showHeader: false,
rowKey: 'category',
columns: [{ title: '分类', dataIndex: 'category' }],
dataSource: [],
};
Object.entries(categoryData).forEach(([category, data]) => {
returnData.dataSource.push({
category,
children: {
showHeader: true,
rowKey: 'name',
columns: expandedRowColumns,
dataSource: data,
},
});
});
data.children = returnData;
} else {
data.children = {
showHeader: true,
rowKey: 'name',
columns: expandedRowColumns,
dataSource: Object.values(categoryData)[0] || [],
};
}
treeData.dataSource.push(data);
});
setTree(treeData);
updateRank([...allSupportMetrics]);
setMetricsList(allSupportMetrics);
});
};
// TODO: 指标选中项更新回调
const metricSelectCallback = ([newMetrics]: [CheckboxStatus[], any]) => {
const updateMetrics: { [key: string]: { metric: string; set: boolean; rank: number }[] } = {};
newMetrics.forEach((metric) => {
const oldMetric = metricsList.find(({ type, name }) => type === metric.type && name === metric.key);
if (oldMetric) {
if (oldMetric.set !== metric.status) {
if (updateMetrics[metric.type]) {
updateMetrics[metric.type].push({ metric: oldMetric.name, set: !oldMetric.set, rank: oldMetric.rank });
} else {
updateMetrics[metric.type] = [{ metric: oldMetric.name, set: !oldMetric.set, rank: oldMetric.rank }];
}
}
}
});
const requestPromise = Object.keys(updateMetrics).length ? setMetricList(updateMetrics) : Promise.resolve();
requestPromise.then(
() => getMetricList(),
() => getMetricList()
);
return requestPromise;
};
useEffect(() => {
onSelectChange(metricsList);
}, [metricsList]);
useEffect(() => {
onRankChange(metricRankList);
}, [metricRankList]);
useEffect(() => {
getMetricList();
}, []);
useImperativeHandle(ref, () => ({
rankChange,
open: () => metricSelectRef.current?.open(),
}));
return tree && <TreeTableDrawer ref={metricSelectRef} tree={tree} checkField="set" submitCallback={metricSelectCallback} />;
});
export default MetricsFilter;

View File

@@ -0,0 +1,233 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { DataNode } from 'knowdesign/lib/basic/tree';
import { useParams } from 'react-router-dom';
import { Tree, Input, Utils, Button } from 'knowdesign';
import api from '@src/api';
import type { ConnectCluster } from '../Connect/AddConnector';
export interface Connector {
connectClusterId: number;
connectClusterName: string;
connectorName: string;
}
interface SelectContentProps {
title: string;
scopeList: {
connectClusters: ConnectCluster[];
connectors: Connector[];
};
isTop?: boolean;
visibleChange?: (v: boolean) => void;
onChange?: (list: any, inputValue: string) => void;
}
interface CheckedNodes {
connectClusters: number[];
connectors: { connectClusterId: number; connectorName: string }[];
}
interface CheckedKeysProps {
checked: React.Key[];
halfChecked: React.Key[];
}
const getParentKey = (key: React.Key, tree: DataNode[]): React.Key => {
let parentKey: React.Key;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey;
};
const SelectContent = (props: SelectContentProps) => {
const { isTop, visibleChange, onChange } = props;
const { clusterId } = useParams<{
clusterId: string;
}>();
const [treeData, setTreeData] = useState<DataNode[]>([]);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [searchValue, setSearchValue] = useState('');
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [checkedKeys, setCheckedKeys] = useState<CheckedKeysProps>({
checked: [],
halfChecked: [],
});
const [checkedNodes, setCheckedNodes] = useState<CheckedNodes>({
connectClusters: [],
connectors: [],
});
const defaultChecked = useRef<CheckedKeysProps>({
checked: [],
halfChecked: [],
});
const onExpand = (newExpandedKeys: string[]) => {
setExpandedKeys(newExpandedKeys);
setAutoExpandParent(false);
};
const onCheck = (keys: CheckedKeysProps, { checkedNodes }: { checkedNodes: DataNode[] }) => {
const returnData: CheckedNodes = {
connectClusters: [],
connectors: [],
};
checkedNodes.map((node) => {
if (node.children) {
returnData.connectClusters.push(node.key as number);
} else {
const [id, ...rest] = (node.key as string).split(':');
returnData.connectors.push({
connectClusterId: Number(id),
connectorName: rest.join(':'),
});
}
});
setCheckedNodes(returnData);
setCheckedKeys(
keys as {
checked: any[];
halfChecked: any[];
}
);
};
const onSubmit = () => {
onChange(checkedNodes, `${checkedNodes.connectClusters.length + checkedNodes.connectors.length}`);
};
const onCancel = () => {
visibleChange(false);
setCheckedKeys(defaultChecked.current);
};
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
let newExpandedKeys: React.Key[] = [];
treeData.forEach((item) => {
if (String(item.title).indexOf(value) > -1) {
newExpandedKeys.push(getParentKey(item.key, treeData));
}
item.children?.forEach((item) => {
if (String(item.title).indexOf(value) > -1) {
newExpandedKeys.push(getParentKey(item.key, treeData));
}
});
});
newExpandedKeys = newExpandedKeys.filter((item, i, self) => item && self.indexOf(item) === i);
setExpandedKeys(newExpandedKeys as React.Key[]);
setSearchValue(value);
setAutoExpandParent(true);
};
const filterTreeData = useMemo(() => {
const loop = (data: DataNode[]): DataNode[] =>
data.map((item) => {
const strTitle = item.title as string;
const index = strTitle.indexOf(searchValue);
const beforeStr = strTitle.substring(0, index);
const afterStr = strTitle.slice(index + searchValue.length);
const title =
index > -1 ? (
<span>
{beforeStr}
<span className="site-tree-search-value">{searchValue}</span>
{afterStr}
</span>
) : (
<span>{strTitle}</span>
);
if (item.children) {
return { title, key: item.key, children: loop(item.children) };
}
return {
title,
key: item.key,
};
});
return loop(treeData);
}, [treeData, searchValue]);
// 获取节点范围列表
const getScopeList = async () => {
const clustersMap: { [key: string]: DataNode } = {};
props.scopeList.connectClusters.forEach((connectCluster) => {
clustersMap[connectCluster.id] = {
title: connectCluster.name,
key: connectCluster.id,
children: [],
};
});
props.scopeList.connectors.forEach((connector) => {
const targetConnectCluster = clustersMap[connector.connectClusterId];
if (targetConnectCluster) {
targetConnectCluster.children.push({
title: connector.connectorName,
key: `${connector.connectClusterId}:${connector.connectorName}`,
});
}
});
setTreeData(Object.values(clustersMap));
};
useEffect(() => {
if (isTop) {
setCheckedKeys({
checked: [],
halfChecked: [],
});
setCheckedNodes({
connectClusters: [],
connectors: [],
});
defaultChecked.current = {
checked: [],
halfChecked: [],
};
}
}, [isTop]);
useEffect(() => {
getScopeList();
}, [props.scopeList]);
return (
<>
<h6 className="time_title">{props.title}</h6>
<div className="custom-scope dashboard-custom-scope">
<div style={{ padding: '0 16px 12px 16px' }}>
<Input size="small" placeholder="请输入内容筛选" onChange={onInputChange} />
</div>
<Tree
className="connect-dashboard-option-tree"
checkable
checkStrictly
treeData={filterTreeData}
checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={onExpand}
onCheck={onCheck}
/>
<div className="btn-con">
<Button type="primary" size="small" className="btn-sure" onClick={onSubmit} disabled={checkedKeys.checked.length === 0}>
</Button>
<Button size="small" onClick={onCancel}>
</Button>
</div>
</div>
</>
);
};
export default SelectContent;

View File

@@ -0,0 +1,13 @@
.dcloud-tree.connect-dashboard-option-tree {
background: transparent;
height: 198px;
padding: 0 10px;
overflow: auto;
.site-tree-search-value {
color: #f50;
}
}
.dd-node-scope-module .flx_con .flx_r .custom-scope.dashboard-custom-scope {
height: 292px;
}

View File

@@ -0,0 +1,259 @@
import React, { useState, useEffect, useRef } from 'react';
import { Utils, AppContainer } from 'knowdesign';
import api, { MetricType } from '@src/api';
import DBreadcrumb from 'knowdesign/es/extend/d-breadcrumb';
import ConnectCard from '@src/components/CardBar/ConnectCard';
import { useParams } from 'react-router-dom';
import { FormattedMetricData, formatChartData, MetricInfo } from '@src/constants/chartConfig';
import ChartOperateBar, { KsHeaderOptions } from '@src/components/ChartOperateBar';
import ChartDetail from '@src/components/DraggableCharts/Detail';
import ChartList from '@src/components/DraggableCharts/ChartList';
import MetricsFilter from './MetricsFilter';
import SelectContent, { Connector } from './SelectContent';
import './index.less';
import { ConnectCluster } from '../Connect/AddConnector';
import HasConnector from '../Connect/HasConnector';
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
const { EventBus } = Utils;
const busInstance = new EventBus();
const DraggableCharts = (): JSX.Element => {
const [global] = AppContainer.useGlobalValue();
const { clusterId } = useParams<{
clusterId: string;
}>();
const [loading, setLoading] = useState<boolean>(true);
// const [scopeList, setScopeList] = useState<IcustomScope[]>([]); // 节点范围列表
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
const [metricList, setMetricList] = useState<{ [key: string]: (string | number)[] }>({});
const [metricChartData, setMetricChartData] = useState<FormattedMetricData[]>([]); // 指标图表数据列表
const [gridNum, setGridNum] = useState<number>(12); // 图表列布局
const [scopeList, setScopeList] = useState<{
connectClusters: ConnectCluster[];
connectors: Connector[];
}>({
connectClusters: [],
connectors: [],
});
const curFetchingTimestamp = useRef(0);
const metricRankList = useRef<string[]>([]);
const metricFilterRef = useRef(null);
const chartDetailRef = useRef(null);
// 根据筛选项获取图表信息
const getMetricChartData = () => {
!curHeaderOptions.isAutoReload && setLoading(true);
const curTimestamp = Date.now();
curFetchingTimestamp.current = curTimestamp;
const [startTime, endTime] = curHeaderOptions.rangeTime;
const { connectClusters, connectors } = curHeaderOptions.scopeData.data;
const isTop = curHeaderOptions?.scopeData?.isTop;
const reqBasicBody = {
startTime,
endTime,
topNu: isTop ? curHeaderOptions.scopeData.data : null,
};
const getConnectClusterMetrics =
metricList[MetricType.Connect] && (isTop || connectClusters?.length)
? Utils.post(
api.getConnectClusterMetrics(clusterId),
Object.assign(
{
...reqBasicBody,
metricsNames: metricList[MetricType.Connect],
},
isTop
? {}
: {
connectClusterIdList: connectClusters,
}
)
)
: Promise.resolve([]);
const getConnectorMetrics =
metricList[MetricType.Connectors] && (isTop || connectors?.length)
? Utils.post(
api.getConnectorMetrics(clusterId),
Object.assign(
{
...reqBasicBody,
metricsNames: metricList[MetricType.Connectors],
},
isTop ? {} : { connectorNameList: connectors }
)
)
: Promise.resolve([]);
Promise.all([getConnectClusterMetrics, getConnectorMetrics]).then(
(res: any) => {
// 如果当前请求不是最新请求,则不做任何操作
if (curFetchingTimestamp.current !== curTimestamp) {
return;
}
// 为保证指标排序结果正确,当指标全部返回后才展示图表
if (res.length === 1 || (res.length === 2 && res[0] && res[1])) {
const connectClusterData = formatChartData(
res[0],
global.getMetricDefine || {},
MetricType.Connect,
curHeaderOptions.rangeTime
) as FormattedMetricData[];
const connectorData = formatChartData(
res[1],
global.getMetricDefine || {},
MetricType.Connectors,
curHeaderOptions.rangeTime
) as FormattedMetricData[];
// 指标排序
const formattedMetricData = [...connectClusterData, ...connectorData];
formattedMetricData.sort((a, b) => metricRankList.current.indexOf(a.metricName) - metricRankList.current.indexOf(b.metricName));
setMetricChartData(formattedMetricData);
} else {
setMetricChartData([]);
}
setLoading(false);
},
() => curFetchingTimestamp.current === curTimestamp && setLoading(false)
);
};
const getScopeList = () => {
const getConnectClusters = Utils.request(api.getConnectClusters(clusterId));
const getConnectors = Utils.request(api.getConnectors(clusterId));
Promise.all([getConnectClusters, getConnectors]).then(([connectClusters, connectors]: [ConnectCluster[], Connector[]]) => {
setScopeList({
connectClusters,
connectors,
});
});
};
// 筛选项变化或者点击刷新按钮
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
const { isAutoReload, gridNum: newGridNum, isRelativeRangeTime, rangeTime, scopeData } = ksOptions;
let newRangeTime = rangeTime;
// 重新渲染图表
if (newGridNum !== gridNum) {
setGridNum(newGridNum || 12);
busInstance.emit('chartResize');
} else {
// 如果为相对时间,则当前时间减去 1 分钟,避免最近一分钟的数据还没采集到时前端多补一个点
if (isRelativeRangeTime) {
newRangeTime = rangeTime.map((timestamp) => timestamp - 60 * 1000) as [number, number];
}
setCurHeaderOptions({
isRelativeRangeTime: isRelativeRangeTime,
isAutoReload: isAutoReload,
rangeTime: newRangeTime,
scopeData: scopeData,
});
}
};
// 图表拖拽
const dragCallback = (oldIndex: number, newIndex: number) => {
const originFrom = metricRankList.current.indexOf(metricChartData[oldIndex].metricName);
const originTarget = metricRankList.current.indexOf(metricChartData[newIndex].metricName);
metricFilterRef.current?.rankChange(originFrom, originTarget);
};
// 展开图表详情
const onExpand = (metricName: string, metricType: MetricType) => {
const linesName =
metricType === MetricType.Connect
? scopeList.connectClusters.map((cluster) => cluster.id)
: scopeList.connectors.map((connector) => ({
connectClusterId: connector.connectClusterId,
connectorName: connector.connectorName,
}));
chartDetailRef.current.onOpen(metricType, metricName, linesName);
};
// 获取图表指标
useEffect(() => {
if (Object.values(metricList).some((list) => list.length) && curHeaderOptions) {
getMetricChartData();
}
}, [curHeaderOptions]);
useEffect(() => {
if (Object.values(metricList).some((list) => list.length) && curHeaderOptions) {
setLoading(true);
getMetricChartData();
}
}, [metricList]);
useEffect(() => {
getScopeList();
}, []);
return (
<div id="dashboard-drag-chart" className="topic-dashboard">
<ChartOperateBar
onChange={ksHeaderChange}
hideNodeScope={false}
openMetricFilter={() => metricFilterRef.current?.open()}
nodeSelect={{
name: 'Connect',
customContent: <SelectContent scopeList={scopeList} title="请选择 Connect 范围" />,
}}
/>
<MetricsFilter
ref={metricFilterRef}
metricType={[MetricType.Connect, MetricType.Connectors]}
onSelectChange={(list) => {
const res: { [key: string]: (string | number)[] } = {};
list.forEach(({ type, name, set }) => {
set && (res[type] ? res[type].push(name) : (res[type] = [name]));
});
setMetricList(res);
}}
onRankChange={(rankList) => {
metricRankList.current = rankList;
}}
/>
<ChartList
busInstance={busInstance}
loading={loading}
gridNum={gridNum}
data={metricChartData}
autoReload={curHeaderOptions?.isAutoReload}
dragCallback={dragCallback}
onExpand={onExpand}
/>
{/* 图表详情 */}
<ChartDetail ref={chartDetailRef} />
</div>
);
};
const ConnectDashboard = (): JSX.Element => {
const [global] = AppContainer.useGlobalValue();
return (
<>
<div className="breadcrumb" style={{ marginBottom: '10px' }}>
<DBreadcrumb
breadcrumbs={[
{ label: '多集群管理', aHref: '/' },
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
{ label: 'Connect', aHref: `` },
]}
/>
</div>
<HasConnector>
<>
<ConnectCard />
<DraggableCharts />
</>
</HasConnector>
</>
);
};
export default ConnectDashboard;

View File

@@ -79,7 +79,7 @@ const BrokerList: React.FC = (props: any) => {
<div style={{ margin: '12px 0' }}>
<ConsumerGroupHealthCheck />
</div>
<div className="clustom-table-content">
<div className="custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div

View File

@@ -181,12 +181,12 @@ const AutoPage = (props: any) => {
<ConsumerGroupHealthCheck></ConsumerGroupHealthCheck>
</div>
)}
<div className={`operating-state ${scene !== 'topicDetail' && 'clustom-table-content'}`}>
<div className={`operating-state ${scene !== 'topicDetail' && 'custom-table-content'}`}>
{/* <CardBar cardColumns={data}></CardBar> */}
{scene !== 'topicDetail' && (
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<Tooltip placement="topLeft" arrowPointAtCenter title='数据刷新间隔为1min可能会有延迟'>
<Tooltip placement="topLeft" arrowPointAtCenter title="数据刷新间隔为1min可能会有延迟">
<div className={`${tableHeaderPrefix}-left-refresh`} onClick={() => searchFn()}>
<IconFont className={`${tableHeaderPrefix}-left-refresh-icon`} type="icon-shuaxin1" />
</div>

View File

@@ -92,7 +92,7 @@
.dcloud-table-container::after {
box-shadow: none !important;
}
.dcloud-pagination{
.dcloud-pagination {
margin-bottom: 0 !important;
}
}
@@ -118,13 +118,14 @@
display: inline-flex;
align-items: center;
&-text,&-text-right{
&-text,
&-text-right {
display: inline-block;
width: 50px;
}
&-text-right{
&-text-right {
text-align: center;
color: #556EE6;
color: #556ee6;
}
&-img {
display: inline-block;
@@ -155,15 +156,18 @@
}
}
.dcloud-checkbox-table-serch{
.dcloud-checkbox-table-serch {
padding-top: 0;
}
.clustom-table-content {
.anticon {
padding: 3px;
border-radius: 50%;
&:hover {
background: rgba(33, 37, 41, 0.04);
.jobs-self {
.dcloud-table-cell {
.anticon {
padding: 3px;
border-radius: 50%;
&:hover {
background: rgba(33, 37, 41, 0.04);
}
}
}
}
}

View File

@@ -173,7 +173,7 @@ const JobsList: React.FC = (props: any) => {
<JobsCheck />
</div>
{/* <Form form={form} layout="inline" onFinish={onFinish}> */}
<div className="clustom-table-content">
<div className="custom-table-content jobs-self">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div

View File

@@ -349,7 +349,7 @@ const LoadBalance: React.FC = (props: any) => {
<LoadRebalanceCardBar trigger={trigger} genData={resetList} filterList={filterList} />
</div>
<div className="load-rebalance-container">
<div className="balance-main clustom-table-content">
<div className="balance-main custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div

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.4K</span>
<span>5.6K</span>
<span>+ Star的项目 Know Streaming</span>
</div>
<div className="carousel-eg-ctr-two-desc desc-two">

View File

@@ -1,12 +1,13 @@
import { Button, Divider, Drawer, Form, Input, InputNumber, Radio, Select, Spin, Space, Utils } from 'knowdesign';
import { Button, Divider, Drawer, Form, Input, InputNumber, Radio, Select, Spin, Space, Utils, Tabs, Collapse, Empty } from 'knowdesign';
import message from '@src/components/Message';
import * as React from 'react';
import React, { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import api from '@src/api';
import { regClusterName, regUsername } from '@src/constants/reg';
import { regClusterName, regIpAndPort, regUsername } from '@src/constants/reg';
import { bootstrapServersErrCodes, jmxErrCodes, zkErrCodes } from './config';
import CodeMirrorFormItem from '@src/components/CodeMirrorFormItem';
import { IconFont } from '@knowdesign/icons';
import notification from '@src/components/Notification';
const LOW_KAFKA_VERSION = '2.8.0';
const CLIENT_PROPERTIES_PLACEHOLDER = `用于创建Kafka客户端进行信息获取的相关配置
例如开启SCRAM-SHA-256安全管控模式的集群需输入如下配置
@@ -20,13 +21,12 @@ word=\\"xxxxxx\\";"
}
`;
const AccessClusters = (props: any): JSX.Element => {
const { afterSubmitSuccess, clusterInfo, visible } = props;
const { Panel } = Collapse;
const ClusterTabContent = forwardRef((props: any, ref): JSX.Element => {
const { form, clusterInfo, visible } = props;
const intl = useIntl();
const [form] = Form.useForm();
const [loading, setLoading] = React.useState(false);
const [confirmLoading, setConfirmLoading] = React.useState(false);
const [curClusterInfo, setCurClusterInfo] = React.useState<any>({});
const [extra, setExtra] = React.useState({
versionExtra: '',
@@ -66,6 +66,7 @@ const AccessClusters = (props: any): JSX.Element => {
const onCancel = () => {
form.resetFields();
setLoading(false);
setCurClusterInfo({});
setExtra({
versionExtra: '',
zooKeeperExtra: '',
@@ -73,60 +74,6 @@ const AccessClusters = (props: any): JSX.Element => {
jmxExtra: '',
});
lastFormItemValue.current = { bootstrapServers: '', zookeeper: '', clientProperties: {} };
props.setVisible && props.setVisible(false);
};
const onSubmit = () => {
form.validateFields().then((res) => {
setConfirmLoading(true);
let clientProperties = null;
try {
clientProperties = res.clientProperties && JSON.parse(res.clientProperties);
} catch (err) {
console.error(err);
}
const params = {
bootstrapServers: res.bootstrapServers,
clientProperties: clientProperties || {},
description: res.description || '',
jmxProperties: {
jmxPort: res.jmxPort,
maxConn: res.maxConn,
openSSL: res.openSSL || false,
token: res.token,
username: res.username,
},
kafkaVersion: res.kafkaVersion,
name: res.name,
zookeeper: res.zookeeper || '',
};
if (!isNaN(curClusterInfo?.id)) {
Utils.put(api.phyCluster, {
...params,
id: curClusterInfo?.id,
})
.then(() => {
message.success('编辑成功');
afterSubmitSuccess && afterSubmitSuccess();
onCancel();
})
.finally(() => {
setConfirmLoading(false);
});
} else {
Utils.post(api.phyCluster, params)
.then(() => {
message.success('集群接入成功。注意新接入集群数据稳定需要1-2分钟');
afterSubmitSuccess && afterSubmitSuccess();
onCancel();
})
.finally(() => {
setConfirmLoading(false);
});
}
});
};
const connectTest = () => {
@@ -213,39 +160,37 @@ const AccessClusters = (props: any): JSX.Element => {
// 获取集群详情数据
React.useEffect(() => {
if (visible) {
if (clusterInfo?.id) {
setLoading(true);
if (clusterInfo?.id && visible) {
setLoading(true);
const resolveJmxProperties = (obj: any) => {
const res = { ...obj };
try {
const originValue = obj?.jmxProperties;
if (originValue) {
const jmxProperties = JSON.parse(originValue);
typeof jmxProperties === 'object' && jmxProperties !== null && Object.assign(res, jmxProperties);
}
} catch (err) {
console.error('jmxProperties not JSON: ', err);
const resolveJmxProperties = (obj: any) => {
const res = { ...obj };
try {
const originValue = obj?.jmxProperties;
if (originValue) {
const jmxProperties = JSON.parse(originValue);
typeof jmxProperties === 'object' && jmxProperties !== null && Object.assign(res, jmxProperties);
}
return res;
};
} catch (err) {
console.error('jmxProperties not JSON: ', err);
}
return res;
};
Utils.request(api.getPhyClusterBasic(clusterInfo.id))
.then((res: any) => {
setCurClusterInfo(resolveJmxProperties(res));
})
.catch((err) => {
setCurClusterInfo(resolveJmxProperties(clusterInfo));
})
.finally(() => {
setLoading(false);
});
} else {
setCurClusterInfo({});
}
Utils.request(api.getPhyClusterBasic(clusterInfo.id))
.then((res: any) => {
setCurClusterInfo(resolveJmxProperties(res));
})
.catch((err) => {
setCurClusterInfo(resolveJmxProperties(clusterInfo));
})
.finally(() => {
setLoading(false);
});
} else {
setCurClusterInfo({});
}
}, [visible, clusterInfo]);
}, [clusterInfo, visible]);
const validators = {
name: async (_: any, value: string) => {
@@ -358,13 +303,510 @@ const AccessClusters = (props: any): JSX.Element => {
},
};
useImperativeHandle(ref, () => ({
onCancel,
}));
return (
<Spin spinning={loading}>
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
<Form.Item
name="name"
label="集群名称"
validateTrigger="onBlur"
rules={[
{
required: true,
validator: validators.name,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="bootstrapServers"
label="Bootstrap Servers"
extra={<span className={!extra.bootstrapExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.bootstrapExtra}</span>}
validateTrigger={'onBlur'}
rules={[
{
required: true,
validator: validators.bootstrapServers,
},
]}
>
<Input.TextArea rows={3} placeholder="请输入Bootstrap Servers地址例如192.168.1.1:9092,192.168.1.2:9092,192.168.1.3:9092" />
</Form.Item>
<Form.Item
name="zookeeper"
label="Zookeeper"
extra={<span className={!extra.zooKeeperExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.zooKeeperExtra}</span>}
validateTrigger={'onBlur'}
rules={[
{
validator: validators.zookeeper,
},
]}
>
<Input.TextArea rows={3} placeholder="请输入Zookeeper地址例如192.168.0.1:2181,192.168.0.2:2181,192.168.0.2:2181/ks-kafka" />
</Form.Item>
<Form.Item className="metrics-form-item" label="Metrics">
<div className="horizontal-form-container">
<div className="inline-items">
<Form.Item name="jmxPort" label="JMX Port :" extra={extra.jmxExtra}>
<InputNumber min={0} max={99999} style={{ width: 129 }} />
</Form.Item>
<Form.Item name="maxConn" label="Max Conn :">
<InputNumber addonAfter="个" min={0} max={99999} style={{ width: 124 }} />
</Form.Item>
</div>
<Form.Item name="openSSL" label="Security :">
<Radio.Group>
<Radio value={false}>None</Radio>
<Radio value={true}>Password Authentication</Radio>
</Radio.Group>
</Form.Item>
<Form.Item dependencies={['openSSL']} noStyle>
{({ getFieldValue }) => {
return getFieldValue('openSSL') ? (
<div className="user-info-form-items">
<Form.Item className="user-info-label" label="User Info :" required />
<div className="inline-items">
<Form.Item
name="username"
rules={[
{
validator: validators.securityUserName,
},
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
className="token-form-item"
name="token"
rules={[
{
validator: validators.securityToken,
},
]}
>
<Input placeholder="请输入密码" />
</Form.Item>
</div>
</div>
) : null;
}}
</Form.Item>
</div>
</Form.Item>
<Form.Item
name="kafkaVersion"
label="Version"
dependencies={['zookeeper']}
extra={<span className="error-extra-info">{extra.versionExtra}</span>}
rules={[
{
required: true,
validator: validators.kafkaVersion,
},
]}
>
<Select placeholder="请选择Kafka Version如无匹配则选择相近版本">
{(props.kafkaVersion || []).map((item: string) => (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="clientProperties"
label="集群配置"
rules={[
{
validator: validators.clientProperties,
},
]}
>
<div>
<CodeMirrorFormItem
resize
defaultInput={form.getFieldValue('clientProperties')}
placeholder={CLIENT_PROPERTIES_PLACEHOLDER}
onBeforeChange={(clientProperties: string) => {
form.setFieldsValue({ clientProperties });
form.validateFields(['clientProperties']);
}}
onBlur={() => {
form.validateFields(['clientProperties']).then(() => {
const bootstrapServers = form.getFieldValue('bootstrapServers');
const zookeeper = form.getFieldValue('zookeeper');
const clientProperties = form.getFieldValue('clientProperties');
if (
clientProperties &&
clientProperties !== lastFormItemValue.current.clientProperties &&
(!!bootstrapServers || !!zookeeper)
) {
connectTest()
.then(() => {
lastFormItemValue.current.clientProperties = clientProperties;
})
.catch(() => {
message.error('连接失败');
});
}
});
}}
/>
</div>
</Form.Item>
<Form.Item
name="description"
label="集群描述"
rules={[
{
validator: validators.description,
},
]}
>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Spin>
);
});
const ConnectorForm = (props: {
initFieldsValue: any;
kafkaVersion: string[];
setSelectedTabKey: React.Dispatch<React.SetStateAction<string>>;
getConnectClustersList: any;
clusterInfo: any;
}) => {
const { initFieldsValue, kafkaVersion, setSelectedTabKey, getConnectClustersList, clusterInfo } = props;
const [form] = Form.useForm();
const validators = {
name: async (_: any, value: string) => {
if (!value) {
return Promise.reject('集群名称不能为空');
}
if (value === initFieldsValue?.name) {
return Promise.resolve();
}
if (!new RegExp(regClusterName).test(value)) {
return Promise.reject('集群名称支持中英文、数字、特殊字符 ! " # $ % & \' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~');
}
return Utils.request(api.getConnectClusterBasicExit(clusterInfo.id, value))
.then((res: any) => {
const data = res || {};
return data?.exist ? Promise.reject('集群名称重复') : Promise.resolve();
})
.catch(() => Promise.reject('连接超时! 请重试或检查服务'));
},
address: async (_: any, value: string) => {
if (!value) {
return Promise.reject('请输入集群地址');
}
if (!new RegExp(regIpAndPort).test(value)) {
return Promise.reject('格式错误正确示例http://1.1.1.1, http://1.1.1.1:65535, https://1.1.1.1, https://1.1.1.1:65535');
}
return Promise.resolve();
},
};
const onFinish = (values: any) => {
const params = {
...values,
id: initFieldsValue?.id,
};
Utils.put(api.batchConnectClusters, [params])
.then((res) => {
// setSelectedTabKey(undefined);
getConnectClustersList();
notification.success({
message: '修改Connect集群成功',
});
})
.catch((error) => {
notification.success({
message: '修改Connect集群失败',
});
});
};
const onCancel = () => {
setSelectedTabKey(undefined);
try {
const jmxPortInfo = JSON.parse(initFieldsValue.jmxProperties) || {};
form.setFieldsValue({ ...initFieldsValue, jmxPort: jmxPortInfo.jmxPort });
} catch {
form.setFieldsValue({ ...initFieldsValue });
}
};
useLayoutEffect(() => {
try {
const jmxPortInfo = JSON.parse(initFieldsValue.jmxProperties) || {};
form.setFieldsValue({ ...initFieldsValue, jmxPort: jmxPortInfo.jmxPort });
} catch {
form.setFieldsValue({ ...initFieldsValue });
}
}, []);
return (
<>
<Drawer
className="drawer-content drawer-access-cluster"
onClose={onCancel}
maskClosable={false}
extra={
<Form form={form} layout="vertical" onFinish={onFinish}>
<Form.Item name="name" label="集群名称" validateTrigger="onBlur" rules={[{ required: true, validator: validators.name }]}>
<Input placeholder="请输入集群名称" maxLength={64} />
</Form.Item>
<Form.Item name="groupName" label="ConsumerGroup Name">
<Input disabled={true} placeholder="请输入 ConsumerGroup Name" />
</Form.Item>
<Form.Item name="clusterUrl" label="集群地址">
<Input disabled placeholder="请输入集群地址" />
</Form.Item>
{/* <Form.Item
name="kafkaVersion"
label="版本号"
rules={[
{
required: true,
message: '请选择版本号',
},
]}
>
<Select placeholder="请选择版本,如无匹配可选择相邻版本">
{(kafkaVersion || []).map((item: string) => (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="JMX Port" name="priority" rules={[{ required: true, message: 'Principle 不能为空' }]} initialValue="throughput">
<Radio.Group>
<Radio value="allBroker">应用于所有Broker</Radio>
<Radio value="givenBroker">应用于特定Broker</Radio>
</Radio.Group>
</Form.Item>
<Form.Item dependencies={['priority']} style={{ marginBottom: 0 }}>
{({ getFieldValue }) =>
getFieldValue('priority') === 'allBroker' ? (
<Form.Item name="jmxPort">
<InputNumber min={0} max={99999} style={{ width: 202 }} />
</Form.Item>
) : (
<Form.Item name="jmxPort">
<Input style={{ width: 202 }} />
</Form.Item>
)
}
</Form.Item> */}
<div className="inline-form-items" style={{ display: 'flex', justifyContent: 'space-between' }}>
<Form.Item
name="version"
label="版本号"
style={{ width: 202 }}
rules={[
{
required: true,
message: '请选择版本号',
},
]}
>
<Select placeholder="请选择版本,如无匹配可选择相邻版本">
{(kafkaVersion || []).map((item: string) => (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="jmxPort" label="JMX Port" style={{ width: 202 }}>
<InputNumber min={0} max={99999} style={{ width: 202 }} />
</Form.Item>
</div>
<Form.Item style={{ marginBottom: 0 }}>
<Space>
<Button type="primary" htmlType="submit" size="small" style={{ width: 56 }}>
</Button>
<Button size="small" style={{ width: 56 }} onClick={onCancel}>
</Button>
</Space>
</Form.Item>
</Form>
</>
);
};
const ConnectTabContent = forwardRef((props: any, ref) => {
const { kafkaVersion, clusterInfo, visible } = props;
const [connectors, setConnectors] = useState<any[]>([]);
const [selectedTabKey, setSelectedTabKey] = useState<string>(undefined);
const [loading, setLoading] = useState(true);
const genExtra = (connector: any) => (
<IconFont
type="icon-shanchu1"
onClick={(e) => {
e.stopPropagation();
Utils.delete(api.deleteConnectClusters, {
params: {
connectClusterId: connector.id,
},
}).then((res) => {
// setSelectedTabKey(undefined);
getConnectClustersList();
notification.success({
message: '删除Connect集群成功',
});
});
}}
/>
);
const getConnectClustersList = () => {
setLoading(true);
Utils.request(api.getConnectClusters(clusterInfo.id))
.then((res: any) => {
setConnectors(res || []);
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
visible && getConnectClustersList();
}, [visible]);
return (
<Spin spinning={loading}>
{connectors?.length ? (
<Collapse
accordion
bordered={false}
activeKey={selectedTabKey}
className="cluster-connect-custom-collapse"
expandIcon={({ isActive }) => <IconFont type="icon-jiantou_1" rotate={isActive ? 90 : 0} />}
onChange={(key: string) => {
setSelectedTabKey(key);
}}
>
{connectors.map((connector, i) => {
return (
<Panel header={connector.name} key={i} className="cluster-connect-custom-panel" extra={genExtra(connector)}>
<ConnectorForm
initFieldsValue={connector}
kafkaVersion={kafkaVersion}
setSelectedTabKey={setSelectedTabKey}
getConnectClustersList={getConnectClustersList}
clusterInfo={clusterInfo}
/>
</Panel>
);
})}
</Collapse>
) : (
<Empty description="暂无Connect集群" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
)}
</Spin>
);
});
interface AccessClusterDrawerProps {
visible: boolean;
setVisible: (visible: boolean) => void;
clusterInfo: any;
afterSubmitSuccess: () => void;
kafkaVersion: string[];
title?: string;
}
const AccessClusterDrawer = (props: AccessClusterDrawerProps) => {
const { afterSubmitSuccess, clusterInfo, visible, setVisible, kafkaVersion } = props;
const intl = useIntl();
const [form] = Form.useForm();
const [confirmLoading, setConfirmLoading] = useState(false);
const clusterRef = useRef(null);
const [positionType, setPositionType] = useState<string>('cluster');
const onCancel = () => {
setPositionType('cluster');
form.resetFields();
clusterRef.current.onCancel();
setVisible && setVisible(false);
};
const callback = (key: any) => {
setPositionType(key);
};
const onSubmit = () => {
form.validateFields().then((res) => {
setConfirmLoading(true);
let clientProperties = null;
try {
clientProperties = res.clientProperties && JSON.parse(res.clientProperties);
} catch (err) {
console.error(err);
}
const params = {
bootstrapServers: res.bootstrapServers,
clientProperties: clientProperties || {},
description: res.description || '',
jmxProperties: {
jmxPort: res.jmxPort,
maxConn: res.maxConn,
openSSL: res.openSSL || false,
token: res.token,
username: res.username,
},
kafkaVersion: res.kafkaVersion,
name: res.name,
zookeeper: res.zookeeper || '',
};
if (!isNaN(clusterInfo?.id)) {
Utils.put(api.phyCluster, {
...params,
id: clusterInfo?.id,
})
.then(() => {
message.success('编辑成功');
afterSubmitSuccess && afterSubmitSuccess();
onCancel();
})
.finally(() => {
setConfirmLoading(false);
});
} else {
Utils.post(api.phyCluster, params)
.then(() => {
message.success('集群接入成功。注意新接入集群数据稳定需要1-2分钟');
afterSubmitSuccess && afterSubmitSuccess();
onCancel();
})
.finally(() => {
setConfirmLoading(false);
});
}
});
};
return (
<Drawer
className="drawer-content drawer-access-cluster"
onClose={onCancel}
maskClosable={false}
extra={
positionType === 'cluster' ? (
<div className="operate-wrap">
<Space>
<Button size="small" onClick={onCancel}>
@@ -376,189 +818,25 @@ const AccessClusters = (props: any): JSX.Element => {
<Divider type="vertical" />
</Space>
</div>
}
title={intl.formatMessage({ id: props.title || clusterInfo?.id ? 'edit.cluster' : 'access.cluster' })}
visible={props.visible}
placement="right"
width={480}
>
<Spin spinning={loading}>
<Form form={form} layout="vertical" onValuesChange={onHandleValuesChange}>
<Form.Item
name="name"
label="集群名称"
validateTrigger="onBlur"
rules={[
{
required: true,
validator: validators.name,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="bootstrapServers"
label="Bootstrap Servers"
extra={<span className={!extra.bootstrapExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.bootstrapExtra}</span>}
validateTrigger={'onBlur'}
rules={[
{
required: true,
validator: validators.bootstrapServers,
},
]}
>
<Input.TextArea
rows={3}
placeholder="请输入Bootstrap Servers地址例如192.168.1.1:9092,192.168.1.2:9092,192.168.1.3:9092"
/>
</Form.Item>
<Form.Item
name="zookeeper"
label="Zookeeper"
extra={<span className={!extra.zooKeeperExtra.includes('连接成功') ? 'error-extra-info' : ''}>{extra.zooKeeperExtra}</span>}
validateTrigger={'onBlur'}
rules={[
{
validator: validators.zookeeper,
},
]}
>
<Input.TextArea
rows={3}
placeholder="请输入Zookeeper地址例如192.168.0.1:2181,192.168.0.2:2181,192.168.0.2:2181/ks-kafka"
/>
</Form.Item>
<Form.Item className="metrics-form-item" label="Metrics">
<div className="horizontal-form-container">
<div className="inline-items">
<Form.Item name="jmxPort" label="JMX Port :" extra={extra.jmxExtra}>
<InputNumber min={0} max={99999} style={{ width: 129 }} />
</Form.Item>
<Form.Item name="maxConn" label="Max Conn :">
<InputNumber addonAfter="个" min={0} max={99999} style={{ width: 124 }} />
</Form.Item>
</div>
<Form.Item name="openSSL" label="Security :">
<Radio.Group>
<Radio value={false}>None</Radio>
<Radio value={true}>Password Authentication</Radio>
</Radio.Group>
</Form.Item>
<Form.Item dependencies={['openSSL']} noStyle>
{({ getFieldValue }) => {
return getFieldValue('openSSL') ? (
<div className="user-info-form-items">
<Form.Item className="user-info-label" label="User Info :" required />
<div className="inline-items">
<Form.Item
name="username"
rules={[
{
validator: validators.securityUserName,
},
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
className="token-form-item"
name="token"
rules={[
{
validator: validators.securityToken,
},
]}
>
<Input placeholder="请输入密码" />
</Form.Item>
</div>
</div>
) : null;
}}
</Form.Item>
</div>
</Form.Item>
<Form.Item
name="kafkaVersion"
label="Version"
dependencies={['zookeeper']}
extra={<span className="error-extra-info">{extra.versionExtra}</span>}
rules={[
{
required: true,
validator: validators.kafkaVersion,
},
]}
>
<Select placeholder="请选择Kafka Version如无匹配则选择相近版本">
{(props.kafkaVersion || []).map((item: string) => (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="clientProperties"
label="集群配置"
rules={[
{
validator: validators.clientProperties,
},
]}
>
<div>
<CodeMirrorFormItem
resize
defaultInput={form.getFieldValue('clientProperties')}
placeholder={CLIENT_PROPERTIES_PLACEHOLDER}
onBeforeChange={(clientProperties: string) => {
form.setFieldsValue({ clientProperties });
form.validateFields(['clientProperties']);
}}
onBlur={(value: any) => {
form.validateFields(['clientProperties']).then(() => {
const bootstrapServers = form.getFieldValue('bootstrapServers');
const zookeeper = form.getFieldValue('zookeeper');
const clientProperties = form.getFieldValue('clientProperties');
if (
clientProperties &&
clientProperties !== lastFormItemValue.current.clientProperties &&
(!!bootstrapServers || !!zookeeper)
) {
connectTest()
.then((res: any) => {
lastFormItemValue.current.clientProperties = clientProperties;
})
.catch((err) => {
message.error('连接失败');
});
}
});
}}
/>
</div>
</Form.Item>
<Form.Item
name="description"
label="集群描述"
rules={[
{
validator: validators.description,
},
]}
>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Spin>
</Drawer>
</>
) : null
}
title={intl.formatMessage({ id: props.title || clusterInfo?.id ? 'edit.cluster' : 'access.cluster' })}
visible={visible}
placement="right"
width={480}
>
<Tabs onChange={callback} activeKey={positionType} defaultActiveKey="cluster">
<Tabs.TabPane tab="Cluster" key="cluster">
<ClusterTabContent ref={clusterRef} form={form} clusterInfo={clusterInfo} kafkaVersion={kafkaVersion} visible={visible} />
</Tabs.TabPane>
{clusterInfo?.id && (
<Tabs.TabPane tab="Connect" key="connect">
<ConnectTabContent kafkaVersion={kafkaVersion} clusterInfo={clusterInfo} visible={visible} />
</Tabs.TabPane>
)}
</Tabs>
</Drawer>
);
};
export default AccessClusters;
export default AccessClusterDrawer;

View File

@@ -466,6 +466,7 @@
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: @font-family-bold;
font-size: 18px;
color: #495057;
@@ -633,6 +634,12 @@
}
}
}
.drawer-access-cluster {
.dcloud-drawer-title {
height: 27px;
line-height: 27px;
}
}
.drawer-content {
.dcloud-form-item-extra {
@@ -674,6 +681,41 @@
}
}
}
.cluster-connect-custom-collapse {
background-color: transparent;
.cluster-connect-custom-panel,
.cluster-connect-custom-panel:last-child {
margin-bottom: 8px;
overflow: hidden;
background: #f8f9fa;
border: 0px;
border-radius: 8px;
.dcloud-collapse-header {
padding: 8px 12px;
font-size: 14px;
color: #495057;
.dcloud-collapse-extra {
opacity: 0;
transition: opacity 0.2s ease;
}
}
&:hover .dcloud-collapse-extra {
opacity: 1;
}
&:not(.dcloud-collapse-item-active) {
.dcloud-collapse-header:hover {
background: #f1f3ff;
}
}
.dcloud-collapse-content-box {
padding: 12px;
}
}
.dcloud-collapse-header .dcloud-collapse-arrow {
margin-right: 8px !important;
font-size: 16px;
}
}
}
.empty-page {

View File

@@ -207,7 +207,7 @@ const SecurityACLs = (): JSX.Element => {
<div className="card-bar">
<ACLsCardBar />
</div>
<div className="security-acls-page-list clustom-table-content">
<div className="security-acls-page-list custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div className={`${tableHeaderPrefix}-left-refresh`} onClick={() => getACLs()}>

View File

@@ -1,20 +1,13 @@
import { Col, Row, SingleChart, Utils, Modal, Spin, Empty, AppContainer, Tooltip } from 'knowdesign';
import { IconFont } from '@knowdesign/icons';
import { SingleChart, Utils, Spin, AppContainer, Tooltip } from 'knowdesign';
import React, { useEffect, useRef, useState } from 'react';
import { arrayMoveImmutable } from 'array-move';
import api from '@src/api';
import { useParams } from 'react-router-dom';
import {
OriginMetricData,
FormattedMetricData,
formatChartData,
supplementaryPoints,
resolveMetricsRank,
MetricInfo,
} from '@src/constants/chartConfig';
import { OriginMetricData, FormattedMetricData, formatChartData, supplementaryPoints } from '@src/constants/chartConfig';
import { MetricType } from '@src/api';
import { getDataUnit } from '@src/constants/chartConfig';
import ChartOperateBar, { KsHeaderOptions } from '@src/components/ChartOperateBar';
import MetricsFilter from '@src/components/ChartOperateBar/MetricSelect';
import RenderEmpty from '@src/components/RenderEmpty';
import DragGroup from '@src/components/DragGroup';
import { getChartConfig } from './config';
@@ -56,7 +49,6 @@ const DEFUALT_METRIC_NEED_METRICS = [DEFAULT_METRIC, 'TotalLogSize', 'TotalProdu
const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
const [global] = AppContainer.useGlobalValue();
const { clusterId } = useParams<{ clusterId: string }>();
const [metricList, setMetricList] = useState<MetricInfo[]>([]); // 指标列表
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
const [metricDataList, setMetricDataList] = useState<any>([]);
const [messagesInMetricData, setMessagesInMetricData] = useState<MessagesInMetric>({
@@ -72,6 +64,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
messagesIn: 0,
other: 0,
});
const metricFilterRef = useRef(null);
// 筛选项变化或者点击刷新按钮
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
@@ -86,65 +79,9 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
});
};
// 更新 rank
const updateRank = (metricList: MetricInfo[]) => {
const { list, listInfo, shouldUpdate } = resolveMetricsRank(metricList);
metricRankList.current = list;
if (shouldUpdate) {
updateMetricList(listInfo);
}
};
// 获取指标列表
const getMetricList = () => {
Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster)).then((res: MetricInfo[] | null) => {
if (!res) return;
const supportMetrics = res.filter((metric) => metric.support);
const selectedMetrics = supportMetrics.filter((metric) => metric.set).map((metric) => metric.name);
!selectedMetrics.includes(DEFAULT_METRIC) && selectedMetrics.push(DEFAULT_METRIC);
updateRank([...supportMetrics]);
setMetricList(supportMetrics);
setSelectedMetricNames(selectedMetrics);
});
};
// 更新指标
const updateMetricList = (metricDetailDTOList: { metric: string; rank: number; set: boolean }[]) => {
return Utils.request(api.getDashboardMetricList(clusterId, MetricType.Cluster), {
method: 'POST',
data: {
metricDetailDTOList,
},
});
};
// 指标选中项更新回调
const indicatorChangeCallback = (newMetricNames: (string | number)[]) => {
const updateMetrics: { metric: string; set: boolean; rank: number }[] = [];
// 需要选中的指标
newMetricNames.forEach(
(name) =>
!selectedMetricNames.includes(name) &&
updateMetrics.push({ metric: name as string, set: true, rank: metricList.find(({ name: metric }) => metric === name)?.rank })
);
// 取消选中的指标
selectedMetricNames.forEach(
(name) =>
!newMetricNames.includes(name) &&
updateMetrics.push({ metric: name as string, set: false, rank: metricList.find(({ name: metric }) => metric === name)?.rank })
);
const requestPromise = Object.keys(updateMetrics).length ? updateMetricList(updateMetrics) : Promise.resolve();
requestPromise.then(
() => getMetricList(),
() => getMetricList()
);
return requestPromise;
};
// 获取 metric 列表的图表数据
const getMetricData = () => {
if (!selectedMetricNames.length) return;
if (!selectedMetricNames?.length) return;
!curHeaderOptions.isAutoReload && setChartLoading(true);
const [startTime, endTime] = curHeaderOptions.rangeTime;
@@ -286,7 +223,7 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
const originTarget = metricRankList.current.indexOf(metricDataList[newIndex].metricName);
const newList = arrayMoveImmutable(metricRankList.current, originFrom, originTarget);
metricRankList.current = newList;
updateMetricList(newList.map((metric, rank) => ({ metric, rank, set: metricList.find(({ name }) => metric === name)?.set || false })));
metricFilterRef.current?.rankChange(originFrom, originTarget);
setMetricDataList(arrayMoveImmutable(metricDataList, oldIndex, newIndex));
};
@@ -302,29 +239,23 @@ const DetailChart = (props: { children: JSX.Element }): JSX.Element => {
}, [curHeaderOptions]);
useEffect(() => {
getMetricList();
setTimeout(() => observeDashboardWidthChange());
}, []);
return (
<div className="chart-panel cluster-detail-container">
<ChartOperateBar
openMetricFilter={() => metricFilterRef.current?.open()}
onChange={ksHeaderChange}
hideNodeScope={true}
hideGridSelect={true}
metricSelect={{
hide: false,
metricType: MetricType.Cluster,
tableData: metricList,
selectedRows: selectedMetricNames,
checkboxProps: (record: MetricInfo) => {
return record.name === DEFAULT_METRIC
? {
disabled: true,
}
: {};
},
submitCallback: indicatorChangeCallback,
/>
<MetricsFilter
ref={metricFilterRef}
metricType={MetricType.Cluster}
onSelectChange={(list, rankList) => {
metricRankList.current = rankList;
setSelectedMetricNames(list);
}}
/>

View File

@@ -32,6 +32,14 @@ export const dimensionMap = {
label: 'Zookeeper',
href: '/zookeeper',
},
5: {
label: 'Connect',
href: '/connect',
},
6: {
label: 'Connector',
href: '/connect/connectors',
},
} as any;
const toLowerCase = (name = '') => {
@@ -78,6 +86,15 @@ const CONFIG_ITEM_DETAIL_DESC = {
SentRate: (valueGroup: any) => {
return `Zookeeper 首发包数小于 ${valueGroup?.ratio * 100}% 总容量`;
},
TaskStartupFailurePercentage: (valueGroup: any) => {
return `任务启动失败概率 小于 ${valueGroup?.value * 100}%`;
},
ConnectorFailedTaskCount: (valueGroup: any) => {
return `失败状态的任务数量 小于 ${valueGroup?.value}`;
},
ConnectorUnassignedTaskCount: (valueGroup: any) => {
return `未被分配的任务数量 小于 ${valueGroup?.value}`;
},
};
export const getConfigItemDetailDesc = (item: keyof typeof CONFIG_ITEM_DETAIL_DESC, valueGroup: any) => {
@@ -145,9 +162,9 @@ export const getDetailColumn = (clusterId: number) => [
// eslint-disable-next-line react/display-name
render: (text: number, record: any) => {
return dimensionMap[text] ? (
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{toLowerCase(record?.dimensionName)}</Link>
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{record?.dimensionDisplayName}</Link>
) : (
toLowerCase(record?.dimensionName)
record?.dimensionDisplayName
);
},
},
@@ -219,9 +236,9 @@ export const getHealthySettingColumn = (form: any, data: any, clusterId: string)
// eslint-disable-next-line react/display-name
render: (text: number, record: any) => {
return dimensionMap[text] ? (
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{toLowerCase(record?.dimensionName)}</Link>
<Link to={`/${systemKey}/${clusterId}${dimensionMap[text]?.href}`}>{record?.dimensionDisplayName}</Link>
) : (
toLowerCase(record?.dimensionName)
record?.dimensionDisplayName
);
},
},
@@ -365,6 +382,33 @@ export const getHealthySettingColumn = (form: any, data: any, clusterId: string)
</div>
);
}
case 'TaskStartupFailurePercentage': {
return (
<div className="table-form-item">
<span className="left-text">{'>'}</span>
{getFormItem({ configItem, percent: true })}
<span className="right-text"></span>
</div>
);
}
case 'ConnectorFailedTaskCount': {
return (
<div className="table-form-item">
<span className="left-text">{'>'}</span>
{getFormItem({ configItem, attrs: { min: 0, max: 99998 } })}
<span className="right-text"></span>
</div>
);
}
case 'ConnectorUnassignedTaskCount': {
return (
<div className="table-form-item">
<span className="left-text">{'>'}</span>
{getFormItem({ configItem, attrs: { min: 0, max: 99998 } })}
<span className="right-text"></span>
</div>
);
}
default: {
return <></>;
}

View File

@@ -149,13 +149,13 @@ const AutoPage = (props: any) => {
title: 'Partitions',
dataIndex: 'partitionNum',
key: 'partitionNum',
width: 95,
width: 100,
},
{
title: 'Replications',
dataIndex: 'replicaNum',
key: 'replicaNum',
width: 95,
width: 100,
},
{
title: '健康状态',
@@ -163,7 +163,7 @@ const AutoPage = (props: any) => {
key: 'HealthState',
sorter: true,
// 设计图上量出来的是144但做的时候发现写144 header部分的sort箭头不出来所以临时调大些
width: 170,
width: 100,
render: (value: any, record: any) => {
return calcCurValue(record, 'HealthState');
},
@@ -289,7 +289,7 @@ const AutoPage = (props: any) => {
<div style={{ margin: '12px 0' }}>
<TopicHealthCheck></TopicHealthCheck>
</div>
<div className="clustom-table-content">
<div className="custom-table-content">
<div className={`${tableHeaderPrefix}`}>
<div className={`${tableHeaderPrefix}-left`}>
{/* 批量扩缩副本 */}

View File

@@ -78,7 +78,7 @@ const ZookeeperList: React.FC = () => {
<div style={{ margin: '12px 0' }}>
<ZookeeperCard />
</div>
<div className="clustom-table-content">
<div className="custom-table-content">
<div className={tableHeaderPrefix}>
<div className={`${tableHeaderPrefix}-left`}>
<div

View File

@@ -25,6 +25,10 @@ import LoadRebalance from './LoadRebalance';
import Zookeeper from './Zookeeper';
import ZookeeperDashboard from './ZookeeperDashboard';
import ConnectDashboard from './ConnectDashboard';
import Connectors from './Connect';
import Workers from './Connect/Workers';
const pageRoutes = [
{
path: '/',
@@ -130,6 +134,24 @@ const pageRoutes = [
component: Zookeeper,
noSider: false,
},
{
path: 'connect',
exact: true,
component: ConnectDashboard,
noSider: false,
},
{
path: 'connect/connectors',
exact: true,
component: Connectors,
noSider: false,
},
{
path: 'connect/workers',
exact: true,
component: Workers,
noSider: false,
},
{
path: 'security/acls',
exact: true,