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

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;