mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-05 13:08:48 +08:00
初始化3.0.0版本
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Drawer, IconFont, Select, Space, Table, Utils } from 'knowdesign';
|
||||
import Api, { MetricType } from '@src/api/index';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default (props: any) => {
|
||||
const { taskPlanData, onClickPreview, onClickSavePreview, brokerList = [] } = props;
|
||||
const routeParams = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
// const [brokerList, setBrokerList] = useState([]);
|
||||
// 存储每个topic的各个partition的编辑状态以及编辑后的目标BrokerID列表
|
||||
const [reassignBrokerIdListEditStatusMap, setReassignBrokerIdListEditStatusMap] = useState<{
|
||||
[key: string]: {
|
||||
[key: number]: {
|
||||
currentBrokerIdList: Array<number>;
|
||||
reassignBrokerIdList: Array<number>;
|
||||
isEdit: boolean;
|
||||
};
|
||||
};
|
||||
}>({});
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
const setCurTopicPartitionEditStatus = (record: any, v: any, status: boolean) => {
|
||||
let reassignBrokerIdListEditStatusMapCopy = JSON.parse(JSON.stringify(reassignBrokerIdListEditStatusMap));
|
||||
reassignBrokerIdListEditStatusMapCopy[record.topicName][v].isEdit = status;
|
||||
setReassignBrokerIdListEditStatusMap(reassignBrokerIdListEditStatusMapCopy);
|
||||
};
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'currentBrokerIdList',
|
||||
render: (a: any) => {
|
||||
return a.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'reassignBrokerIdList',
|
||||
render: (a: any) => {
|
||||
return a.join(',');
|
||||
},
|
||||
},
|
||||
];
|
||||
useEffect(() => {
|
||||
let mapObj = taskPlanData.reduce((acc: any, cur: any) => {
|
||||
acc[cur.topicName] = cur.partitionPlanList.reduce((pacc: any, pcur: any) => {
|
||||
pacc[pcur.partitionId] = {
|
||||
currentBrokerIdList: pcur.currentBrokerIdList,
|
||||
reassignBrokerIdList: pcur.reassignBrokerIdList,
|
||||
isEdit: false,
|
||||
};
|
||||
return pacc;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
setReassignBrokerIdListEditStatusMap(mapObj);
|
||||
}, [taskPlanData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
style={{ marginTop: 20 }}
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
onClickPreview && onClickPreview(taskPlanData);
|
||||
}}
|
||||
>
|
||||
预览任务计划
|
||||
</Button>
|
||||
<Drawer
|
||||
title={
|
||||
<Space size={0}>
|
||||
<Button
|
||||
className="drawer-title-left-button"
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconFont type="icon-fanhui1" />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Divider type="vertical" />
|
||||
<span style={{ paddingLeft: '5px' }}>任务计划</span>
|
||||
</Space>
|
||||
}
|
||||
width={600}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className="preview-task-plan-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
// closeIcon={<ArrowLeftOutlined />}
|
||||
>
|
||||
<Table
|
||||
rowKey={'topicName'}
|
||||
dataSource={taskPlanData}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowRender: (topicRecord) => {
|
||||
let partitionPlanList = topicRecord.partitionPlanList;
|
||||
const columns = [
|
||||
{
|
||||
title: 'Partition',
|
||||
dataIndex: 'partitionId',
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'currentBrokerIdList',
|
||||
render: (a: any) => {
|
||||
return a.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'reassignBrokerIdList',
|
||||
render: (a: any, partitionRecord: any) => {
|
||||
let isEdit = reassignBrokerIdListEditStatusMap[topicRecord.topicName][partitionRecord.partitionId].isEdit;
|
||||
let reassignBrokerIdList =
|
||||
reassignBrokerIdListEditStatusMap[topicRecord.topicName][partitionRecord.partitionId].reassignBrokerIdList;
|
||||
return isEdit ? (
|
||||
<Select
|
||||
value={reassignBrokerIdList}
|
||||
mode="multiple"
|
||||
onChange={(selBrokerIds) => {
|
||||
let reassignBrokerIdListEditStatusMapCopy = JSON.parse(JSON.stringify(reassignBrokerIdListEditStatusMap));
|
||||
reassignBrokerIdListEditStatusMapCopy[topicRecord.topicName][partitionRecord.partitionId].reassignBrokerIdList =
|
||||
selBrokerIds;
|
||||
setReassignBrokerIdListEditStatusMap(reassignBrokerIdListEditStatusMapCopy);
|
||||
}}
|
||||
style={{ width: '122px' }}
|
||||
>
|
||||
{brokerList.length > 0 &&
|
||||
brokerList?.map((broker: any) => (
|
||||
<Option key={Math.random()} value={broker.brokerId}>
|
||||
{broker.brokerId}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
a.join(',')
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'partitionId',
|
||||
render: (v: any) => {
|
||||
let isEdit = reassignBrokerIdListEditStatusMap[topicRecord.topicName][v].isEdit;
|
||||
return isEdit ? (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
onClickSavePreview && onClickSavePreview(reassignBrokerIdListEditStatusMap);
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setCurTopicPartitionEditStatus(topicRecord, v, false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setCurTopicPartitionEditStatus(topicRecord, v, true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="partition-task-plan-list">
|
||||
<Table
|
||||
rowKey={'partitionId'}
|
||||
dataSource={partitionPlanList}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
simple: true,
|
||||
}}
|
||||
></Table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}}
|
||||
></Table>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,544 @@
|
||||
// 批量扩缩副本
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Table,
|
||||
Tag,
|
||||
Utils,
|
||||
AppContainer,
|
||||
message,
|
||||
Divider,
|
||||
Space,
|
||||
} from 'knowdesign';
|
||||
import './index.less';
|
||||
import Api, { MetricType } from '@src/api/index';
|
||||
import moment from 'moment';
|
||||
import PreviewTaskPlan from './PreviewTaskPlan';
|
||||
import type { RangePickerProps } from 'knowdesign/es/basic/date-picker';
|
||||
import { timeFormat } from '@src/constants/common';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
const jobNameMap: any = {
|
||||
expandAndReduce: '批量扩缩容',
|
||||
transfer: '批量迁移',
|
||||
};
|
||||
|
||||
interface DefaultConfig {
|
||||
jobId?: number | string;
|
||||
type?: string;
|
||||
topics: Array<any>;
|
||||
drawerVisible: boolean;
|
||||
onClose: () => void;
|
||||
genData?: () => any;
|
||||
jobStatus?: number;
|
||||
}
|
||||
|
||||
export default (props: DefaultConfig) => {
|
||||
const { type = 'expandAndReduce', topics, drawerVisible, onClose, jobId, genData, jobStatus } = props;
|
||||
const routeParams = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [topicData, setTopicData] = useState([]);
|
||||
const [brokerList, setBrokerList] = useState([]);
|
||||
const [taskPlanData, setTaskPlanData] = useState([]);
|
||||
const [selectBrokerList, setSelectBrokerList] = useState([]);
|
||||
const [topicNewReplicas, setTopicNewReplicas] = useState([]);
|
||||
const [form] = Form.useForm();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loadingTopic, setLoadingTopic] = useState<boolean>(true);
|
||||
const [targetNodeVisible, setTargetNodeVisible] = useState(false);
|
||||
const [topicMetaData, setTopicMetaData] = useState([]);
|
||||
const [topicSelectValue, setTopicSelectValue] = useState(topics);
|
||||
|
||||
const topicDataColumns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
},
|
||||
{
|
||||
title: '近三天平均流量',
|
||||
dataIndex: 'latestDaysAvgBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-wrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div key={index} className="custom-tag">
|
||||
{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '近三天峰值流量&时间',
|
||||
dataIndex: 'latestDaysMaxBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-wrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div key={index} className="custom-tag">
|
||||
<div>{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}</div>
|
||||
<div className="time">{item && item.timeStamp ? moment(item.timeStamp * 1000).format('HH:mm:ss') : '-'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Partition数',
|
||||
dataIndex: 'partitionNum',
|
||||
},
|
||||
{
|
||||
title: '当前副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
},
|
||||
{
|
||||
title: '最终副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (v: any, _r: any, index: number) => {
|
||||
return (
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={brokerList.length || 1}
|
||||
value={topicNewReplicas[index]}
|
||||
defaultValue={topicNewReplicas[index]}
|
||||
onChange={(v) => {
|
||||
if (v > topicData[index]?.replicaNum) {
|
||||
setTargetNodeVisible(true);
|
||||
} else if (
|
||||
v <= topicData[index]?.replicaNum &&
|
||||
topicData.filter((item, key) => topicNewReplicas[key] && item.replicaNum < topicNewReplicas[key]).length < 1
|
||||
) {
|
||||
setTargetNodeVisible(false);
|
||||
}
|
||||
const topicNewReplicasCopy = JSON.parse(JSON.stringify(topicNewReplicas));
|
||||
topicNewReplicasCopy[index] = v;
|
||||
setTopicNewReplicas(topicNewReplicasCopy);
|
||||
}}
|
||||
></InputNumber>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const onDrawerClose = () => {
|
||||
form.resetFields();
|
||||
setTopicData([]);
|
||||
setSelectBrokerList([]);
|
||||
// setLoadingTopic(true);
|
||||
setVisible(false);
|
||||
setTargetNodeVisible(false);
|
||||
setTopicNewReplicas([]);
|
||||
onClose();
|
||||
};
|
||||
const getReassignmentList = (topiclist?: any) => {
|
||||
return Utils.post(Api.getReassignmentList(), {
|
||||
clusterId: Number(routeParams.clusterId),
|
||||
topicNameList: topiclist,
|
||||
});
|
||||
};
|
||||
const getJobsTaskData = () => {
|
||||
const params = {
|
||||
clusterId: routeParams.clusterId,
|
||||
jobId: jobId,
|
||||
};
|
||||
return Utils.request(Api.getJobsTaskData(params.clusterId, params.jobId), params);
|
||||
};
|
||||
const getTaskPlanData = (params: any) => {
|
||||
return Utils.post(Api.getTaskPlanData(), params);
|
||||
};
|
||||
const onClickPreview = (data?: any) => {
|
||||
if (!targetNodeVisible) {
|
||||
const planParams = topicData.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: [],
|
||||
clusterId: routeParams.clusterId,
|
||||
newReplicaNum: topicNewReplicas[index],
|
||||
topicName: item.topicName,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
}
|
||||
if (selectBrokerList.length === 0) return;
|
||||
if (topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
!data &&
|
||||
form.validateFields(['brokerList']).then((e) => {
|
||||
const planParams = topicSelectValue.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: selectBrokerList,
|
||||
clusterId: routeParams.clusterId,
|
||||
newReplicaNum: topicNewReplicas[index] || item.replicaNum,
|
||||
topicName: item,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
});
|
||||
};
|
||||
const onClickSavePreview = (data: any) => {
|
||||
const taskPlanDataCopy = JSON.parse(JSON.stringify(taskPlanData));
|
||||
const hasError: any[] = [];
|
||||
taskPlanDataCopy.forEach((topic: any, index: number) => {
|
||||
const partitionIds = Object.keys(data[topic.topicName]);
|
||||
const newReassignBrokerIdList = partitionIds.reduce((acc: Array<number>, cur: string) => {
|
||||
const ressignBrokerIdList = data[topic.topicName][cur].reassignBrokerIdList;
|
||||
if (ressignBrokerIdList.length !== topicNewReplicas[index]) {
|
||||
hasError.push(topic.topicName + ' Partition ' + cur);
|
||||
}
|
||||
acc.push(...ressignBrokerIdList);
|
||||
return acc;
|
||||
}, []);
|
||||
topic.reassignBrokerIdList = Array.from(new Set(newReassignBrokerIdList));
|
||||
topic.partitionPlanList.forEach((partition: any) => {
|
||||
partition.reassignBrokerIdList = data[topic.topicName][partition.partitionId].reassignBrokerIdList;
|
||||
});
|
||||
});
|
||||
if (hasError.length) {
|
||||
message.error(hasError.join(',') + '副本数与目标节点数不一致');
|
||||
} else {
|
||||
setTaskPlanData(taskPlanDataCopy);
|
||||
}
|
||||
};
|
||||
const checkRep = (_: any, value: any[]) => {
|
||||
if (value && value.length && topicNewReplicas.find((rep) => rep && rep > value.length)) {
|
||||
return Promise.reject('节点数低于Topic最大副本数');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
|
||||
// 不能选择小于当前时间
|
||||
return current && current <= moment().add(-1, 'days').endOf('day');
|
||||
};
|
||||
const range = (start: number, end: number) => {
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const disabledDateTime = (current: any) => {
|
||||
return {
|
||||
disabledHours: () => (current > moment() ? [] : range(0, moment().hour())),
|
||||
disabledMinutes: () => (current > moment() ? [] : range(0, moment().add(1, 'minute').minute())),
|
||||
// disabledSeconds: () => [55, 56],
|
||||
};
|
||||
};
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const planParams = topicData.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: [],
|
||||
clusterId: routeParams.clusterId,
|
||||
newReplicaNum: item.replicaNum,
|
||||
topicName: item.topicName,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
}
|
||||
}, [topics]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue;
|
||||
}, [topics]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
onClickPreview();
|
||||
}, [selectBrokerList, topicNewReplicas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
if (jobId) {
|
||||
setLoadingTopic(true);
|
||||
getJobsTaskData()
|
||||
.then((res: any) => {
|
||||
const jobData = (res && JSON.parse(res.jobData)) || {};
|
||||
const planTime = res?.planTime && moment(res.planTime, timeFormat);
|
||||
const { topicPlanList = [], throttleUnitB = 0, jobDesc = '' } = jobData;
|
||||
let selectedBrokerList: any[] = [];
|
||||
const topicData = topicPlanList.map((topic: any) => {
|
||||
selectedBrokerList = topic.reassignBrokerIdList;
|
||||
return {
|
||||
...topic,
|
||||
topicName: topic.topicName,
|
||||
latestDaysAvgBytesInList: topic.latestDaysAvgBytesInList || [],
|
||||
latestDaysMaxBytesInList: topic.latestDaysMaxBytesInList || [],
|
||||
partitionIdList: topic.partitionIdList,
|
||||
replicaNum: topic.presentReplicaNum,
|
||||
retentionMs: topic.originalRetentionTimeUnitMs,
|
||||
};
|
||||
});
|
||||
setTopicData(topicData);
|
||||
const newReplica = topicPlanList.map((t: any) => t.newReplicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
// const needMovePartitions = topicPlanList.map((t: any) => t.partitionIdList || []);
|
||||
// setNeedMovePartitions(needMovePartitions);
|
||||
// const MoveDataTimeRanges = topicPlanList.map((t: any) => {
|
||||
// const timeHour = t.reassignRetentionTimeUnitMs / 1000 / 60 / 60;
|
||||
// return timeHour > 1 ? Math.floor(timeHour) : timeHour.toFixed(2);
|
||||
// });
|
||||
// setMoveDataTimeRanges(MoveDataTimeRanges);
|
||||
setSelectBrokerList(selectedBrokerList);
|
||||
|
||||
form.setFieldsValue({
|
||||
brokerList: selectedBrokerList,
|
||||
throttle: throttleUnitB / 1024 / 1024,
|
||||
planTime,
|
||||
description: res?.jobDesc,
|
||||
topicList: topicSelectValue,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
setVisible(true);
|
||||
Utils.request(Api.getDashboardMetadata(routeParams.clusterId, MetricType.Broker)).then((res: any) => {
|
||||
setBrokerList(res || []);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
!jobId &&
|
||||
Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
|
||||
.then((res: any) => {
|
||||
const filterRes = res.filter((item: any) => item.type !== 1);
|
||||
const topics = (filterRes || []).map((item: any) => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
partitionIdList: item.partitionIdList,
|
||||
};
|
||||
});
|
||||
setTopicMetaData(topics);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobId) {
|
||||
setLoadingTopic(true);
|
||||
drawerVisible &&
|
||||
getReassignmentList(topicSelectValue)
|
||||
.then((res: any[]) => {
|
||||
setTopicData(res);
|
||||
const newReplica = res.map((t) => t.replicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [topicSelectValue, drawerVisible]);
|
||||
|
||||
const addReassign = () => {
|
||||
// if (selectBrokerList.length && topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
form.validateFields().then((e) => {
|
||||
const formData = form.getFieldsValue();
|
||||
const handledData = {
|
||||
creator: global.userInfo.userName,
|
||||
jobType: 1, // type 0 topic迁移 1 扩缩容 2集群均衡
|
||||
planTime: formData.planTime,
|
||||
jobStatus: jobId ? jobStatus : 2, //status 2 创建
|
||||
target: topicSelectValue.join(','),
|
||||
id: jobId || '',
|
||||
jobDesc: formData.description,
|
||||
jobData: JSON.stringify({
|
||||
clusterId: routeParams.clusterId,
|
||||
jobDesc: formData.description,
|
||||
throttleUnitB: formData.throttle * 1024 * 1024,
|
||||
topicPlanList: topicSelectValue.map((topic, index) => {
|
||||
return {
|
||||
clusterId: routeParams.clusterId,
|
||||
topicName: topic,
|
||||
partitionIdList: topic.partitionIdList,
|
||||
partitionNum: topicData[index].partitionNum,
|
||||
presentReplicaNum: topicData[index].replicaNum,
|
||||
newReplicaNum: topicNewReplicas[index],
|
||||
originalBrokerIdList: taskPlanData[index].currentBrokerIdList,
|
||||
reassignBrokerIdList: taskPlanData[index].reassignBrokerIdList,
|
||||
originalRetentionTimeUnitMs: topicData[index].retentionMs,
|
||||
reassignRetentionTimeUnitMs: topicData[index].retentionMs,
|
||||
latestDaysAvgBytesInList: topicData[index].latestDaysAvgBytesInList,
|
||||
latestDaysMaxBytesInList: topicData[index].latestDaysMaxBytesInList,
|
||||
partitionPlanList: taskPlanData[index].partitionPlanList,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
};
|
||||
if (jobId) {
|
||||
Utils.put(Api.putJobsTaskData(routeParams.clusterId), handledData)
|
||||
.then(() => {
|
||||
message.success('扩缩副本任务编辑成功');
|
||||
onDrawerClose();
|
||||
genData();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err, 'err');
|
||||
});
|
||||
} else {
|
||||
Utils.post(Api.createTask(routeParams.clusterId), handledData)
|
||||
.then(() => {
|
||||
message.success('扩缩副本任务创建成功');
|
||||
onDrawerClose();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title={jobNameMap[type]}
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onDrawerClose}
|
||||
visible={visible}
|
||||
className="topic-job-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={(_) => {
|
||||
// setVisible(false);
|
||||
onDrawerClose();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={addReassign}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="wrap">
|
||||
<h4 className="title">{jobNameMap[type]}Topic</h4>
|
||||
|
||||
{!jobId && (
|
||||
<Form form={form}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item>
|
||||
<Select
|
||||
placeholder="请选择Topic,可多选"
|
||||
mode="multiple"
|
||||
onChange={(v: any) => {
|
||||
setTopicSelectValue(v);
|
||||
}}
|
||||
options={topicMetaData}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
)}
|
||||
<Table dataSource={topicData} columns={topicDataColumns} pagination={false} loading={loadingTopic} />
|
||||
<Form form={form} layout="vertical" className="task-form">
|
||||
<Row>
|
||||
{targetNodeVisible && (
|
||||
<Col span={12}>
|
||||
<Form.Item name="brokerList" label="目标节点" rules={[{ required: true }, { validator: checkRep }]}>
|
||||
<Select
|
||||
placeholder="请选择Broker,可多选"
|
||||
mode="multiple"
|
||||
onChange={(v: any) => {
|
||||
setSelectBrokerList(v);
|
||||
}}
|
||||
>
|
||||
{brokerList.map((item, index) => (
|
||||
<Option key={index} value={item.brokerId}>
|
||||
{item.brokerId}
|
||||
{`(${item.host})`}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={1}>
|
||||
{/* taskPlanData是传给组件的初始值
|
||||
点击预览任务计划,触发onClickPreview回调,发起请求获取taskPlanData
|
||||
组件内部改完点击每一行保存时,再通过onClickSavePreview回调向外派发数据 */}
|
||||
<PreviewTaskPlan
|
||||
taskPlanData={taskPlanData}
|
||||
onClickPreview={onClickPreview}
|
||||
onClickSavePreview={onClickSavePreview}
|
||||
brokerList={brokerList}
|
||||
></PreviewTaskPlan>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<h4 className="title">迁移任务配置</h4>
|
||||
<Row gutter={32} className="topic-execution-time">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="throttle"
|
||||
label="限流"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (r: any, v: number) => {
|
||||
if ((v || v === 0) && v <= 0) {
|
||||
return Promise.reject('限流值不能小于等于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} max={99999} addonAfter="MB/S"></InputNumber>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="planTime" label="任务执行时间" rules={[{ required: true }]}>
|
||||
<DatePicker
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
showTime
|
||||
style={{ width: '100%' }}
|
||||
disabledDate={disabledDate}
|
||||
disabledTime={disabledDateTime}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4 className="title">描述</h4>
|
||||
<Form.Item name="description" label="任务描述" rules={[{ required: true }]}>
|
||||
<TextArea placeholder="暂支持 String 格式" style={{ height: 110 }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,555 @@
|
||||
// 批量迁移
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Table,
|
||||
Utils,
|
||||
AppContainer,
|
||||
message,
|
||||
Space,
|
||||
Divider,
|
||||
Transfer,
|
||||
IconFont,
|
||||
} from 'knowdesign';
|
||||
import './index.less';
|
||||
import Api, { MetricType } from '@src/api/index';
|
||||
import moment from 'moment';
|
||||
import PreviewTaskPlan from './PreviewTaskPlan';
|
||||
import { timeFormat } from '@src/lib/utils';
|
||||
import type { RangePickerProps } from 'knowdesign/es/basic/date-picker';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
const jobNameMap: any = {
|
||||
expandAndReduce: '批量扩缩容',
|
||||
transfer: '批量迁移',
|
||||
};
|
||||
|
||||
interface DefaultConfig {
|
||||
jobId?: number | string;
|
||||
type?: string;
|
||||
topics: Array<any>;
|
||||
drawerVisible: boolean;
|
||||
onClose: () => void;
|
||||
genData?: () => any;
|
||||
jobStatus?: number;
|
||||
}
|
||||
|
||||
export default (props: DefaultConfig) => {
|
||||
const { type = 'transfer', topics, drawerVisible, onClose, jobId, genData, jobStatus } = props;
|
||||
const routeParams = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = useState(drawerVisible);
|
||||
const [topicData, setTopicData] = useState([]);
|
||||
const [brokerList, setBrokerList] = useState([]);
|
||||
const [taskPlanData, setTaskPlanData] = useState([]);
|
||||
const [selectBrokerList, setSelectBrokerList] = useState([]);
|
||||
const [topicNewReplicas, setTopicNewReplicas] = useState([]);
|
||||
const [needMovePartitions, setNeedMovePartitions] = useState([]);
|
||||
const [moveDataTimeRanges, setMoveDataTimeRanges] = useState([]);
|
||||
const [form] = Form.useForm();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loadingTopic, setLoadingTopic] = useState<boolean>(true);
|
||||
const [topicMetaData, setTopicMetaData] = useState([]);
|
||||
const [topicSelectValue, setTopicSelectValue] = useState(topics);
|
||||
|
||||
const topicDataColumns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
},
|
||||
{
|
||||
title: '近三天平均流量',
|
||||
dataIndex: 'latestDaysAvgBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-nowrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div key={index} className="custom-tag">
|
||||
{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '近三天峰值流量&时间',
|
||||
dataIndex: 'latestDaysMaxBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-wrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div className="custom-tag" key={index}>
|
||||
<div>{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}</div>
|
||||
<div className="time">{item && item.timeStamp ? moment(item.timeStamp * 1000).format('HH:mm:ss') : '-'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '需迁移Partition',
|
||||
dataIndex: 'partitionIdList',
|
||||
render: (v: any, r: any, i: number) => {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="下拉多选分区ID"
|
||||
defaultValue={v}
|
||||
value={needMovePartitions[i]}
|
||||
mode="multiple"
|
||||
onChange={(a: any) => {
|
||||
const needMovePartitionsCopy = JSON.parse(JSON.stringify(needMovePartitions));
|
||||
needMovePartitionsCopy[i] = a;
|
||||
setNeedMovePartitions(needMovePartitionsCopy);
|
||||
}}
|
||||
>
|
||||
{v.map((p: any, index: any) => (
|
||||
<Option key={index} value={p}>
|
||||
{p}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
},
|
||||
{
|
||||
title: '数据保存时间',
|
||||
dataIndex: 'retentionMs',
|
||||
render: (v: any) => {
|
||||
return timeFormat(v);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '迁移数据时间范围',
|
||||
dataIndex: 'newRetentionMs',
|
||||
render: (v: any, r: any, i: number) => {
|
||||
return (
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={99999}
|
||||
defaultValue={moveDataTimeRanges[i]}
|
||||
value={moveDataTimeRanges[i]}
|
||||
onChange={(n: number) => {
|
||||
const moveDataTimeRangesCopy = JSON.parse(JSON.stringify(moveDataTimeRanges));
|
||||
moveDataTimeRangesCopy[i] = n;
|
||||
setMoveDataTimeRanges(moveDataTimeRangesCopy);
|
||||
}}
|
||||
formatter={(value) => (value ? `${value} h` : '')}
|
||||
// @ts-ignore
|
||||
parser={(value) => value.replace('h', '')}
|
||||
></InputNumber>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const onDrawerClose = () => {
|
||||
form.resetFields();
|
||||
setTopicData([]);
|
||||
setSelectBrokerList([]);
|
||||
setTopicNewReplicas([]);
|
||||
setNeedMovePartitions([]);
|
||||
setMoveDataTimeRanges([]);
|
||||
// setLoadingTopic(true);
|
||||
setVisible(false);
|
||||
onClose();
|
||||
};
|
||||
const getReassignmentList = (topiclist?: any) => {
|
||||
return Utils.post(Api.getReassignmentList(), {
|
||||
clusterId: Number(routeParams.clusterId),
|
||||
topicNameList: topiclist,
|
||||
});
|
||||
};
|
||||
const getJobsTaskData = () => {
|
||||
const params = {
|
||||
clusterId: routeParams.clusterId,
|
||||
jobId: jobId,
|
||||
};
|
||||
return Utils.request(Api.getJobsTaskData(params.clusterId, params.jobId), params);
|
||||
};
|
||||
const getTaskPlanData = (params: any) => {
|
||||
return Utils.post(Api.getMovePlanTaskData(), params);
|
||||
};
|
||||
const onClickPreview = (data?: any) => {
|
||||
if (selectBrokerList.length === 0) return;
|
||||
if (topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
!data &&
|
||||
form.validateFields(['brokerList']).then((e) => {
|
||||
const planParams = topicSelectValue.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: selectBrokerList,
|
||||
clusterId: routeParams.clusterId,
|
||||
enableRackAwareness: false,
|
||||
newReplicaNum: topicNewReplicas[index],
|
||||
partitionIdList: needMovePartitions[index],
|
||||
topicName: item,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
});
|
||||
};
|
||||
const onClickSavePreview = (data: any) => {
|
||||
const taskPlanDataCopy = JSON.parse(JSON.stringify(taskPlanData));
|
||||
const hasError: any[] = [];
|
||||
taskPlanDataCopy.forEach((topic: any, index: number) => {
|
||||
const partitionIds = Object.keys(data[topic.topicName]);
|
||||
const newReassignBrokerIdList = partitionIds.reduce((acc: Array<number>, cur: string) => {
|
||||
const ressignBrokerIdList = data[topic.topicName][cur].reassignBrokerIdList;
|
||||
if (ressignBrokerIdList.length !== topicNewReplicas[index]) {
|
||||
hasError.push(topic.topicName + ' Partition ' + cur);
|
||||
}
|
||||
acc.push(...ressignBrokerIdList);
|
||||
return acc;
|
||||
}, []);
|
||||
topic.reassignBrokerIdList = Array.from(new Set(newReassignBrokerIdList));
|
||||
topic.partitionPlanList.forEach((partition: any) => {
|
||||
partition.reassignBrokerIdList = data[topic.topicName][partition.partitionId].reassignBrokerIdList;
|
||||
});
|
||||
});
|
||||
if (hasError.length) {
|
||||
message.error(hasError.join(',') + '副本数与目标节点数不一致');
|
||||
} else {
|
||||
setTaskPlanData(taskPlanDataCopy);
|
||||
}
|
||||
};
|
||||
const checkRep = (_: any, value: any[]) => {
|
||||
if (value && value.length && topicNewReplicas.find((rep) => rep && rep > value.length)) {
|
||||
return Promise.reject('节点数低于Topic最大副本数');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
|
||||
// 不能选择小于当前时间
|
||||
return current && current <= moment().add(-1, 'days').endOf('day');
|
||||
};
|
||||
const range = (start: number, end: number) => {
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const disabledDateTime = (current: any) => {
|
||||
return {
|
||||
disabledHours: () => (current > moment() ? [] : range(0, moment().hour())),
|
||||
disabledMinutes: () => (current > moment() ? [] : range(0, moment().add(1, 'minute').minute())),
|
||||
// disabledSeconds: () => [55, 56],
|
||||
};
|
||||
};
|
||||
|
||||
const nodeChange = (val: any) => {
|
||||
setSelectBrokerList(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
onClickPreview();
|
||||
}, [selectBrokerList, needMovePartitions, moveDataTimeRanges]);
|
||||
|
||||
useEffect(() => {
|
||||
if (topics.length === 0 || !drawerVisible) return;
|
||||
if (jobId) {
|
||||
setLoadingTopic(true);
|
||||
getJobsTaskData()
|
||||
.then((res: any) => {
|
||||
const jobData = (res && JSON.parse(res.jobData)) || {};
|
||||
const planTime = res?.planTime && moment(res.planTime, 'YYYY-MM-DD HH:mm:ss');
|
||||
const { topicPlanList = [], throttleUnitB = 0, jobDesc = '' } = jobData;
|
||||
let selectedBrokerList: any[] = [];
|
||||
const topicData = topicPlanList.map((topic: any) => {
|
||||
selectedBrokerList = topic.reassignBrokerIdList;
|
||||
return {
|
||||
...topic,
|
||||
topicName: topic.topicName,
|
||||
latestDaysAvgBytesInList: topic.latestDaysAvgBytesInList || [],
|
||||
latestDaysMaxBytesInList: topic.latestDaysMaxBytesInList || [],
|
||||
partitionIdList: topic.partitionIdList,
|
||||
replicaNum: topic.presentReplicaNum,
|
||||
retentionMs: topic.originalRetentionTimeUnitMs,
|
||||
// newRetentionMs: topic.reassignRetentionTimeUnitMs,
|
||||
};
|
||||
});
|
||||
setTopicData(topicData);
|
||||
const newReplica = topicPlanList.map((t: any) => t.newReplicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
const needMovePartitions = topicPlanList.map((t: any) => t.partitionIdList || []);
|
||||
setNeedMovePartitions(needMovePartitions);
|
||||
const MoveDataTimeRanges = topicPlanList.map((t: any) => {
|
||||
const timeHour = t.reassignRetentionTimeUnitMs / 1000 / 60 / 60;
|
||||
return timeHour > 1 ? Math.floor(timeHour) : timeHour.toFixed(2);
|
||||
});
|
||||
setMoveDataTimeRanges(MoveDataTimeRanges);
|
||||
setSelectBrokerList(selectedBrokerList);
|
||||
form.setFieldsValue({
|
||||
brokerList: selectedBrokerList,
|
||||
throttle: throttleUnitB / 1024 / 1024,
|
||||
planTime,
|
||||
description: res?.jobDesc,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
!jobId &&
|
||||
drawerVisible &&
|
||||
Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
|
||||
.then((res: any) => {
|
||||
const filterRes = res.filter((item: any) => item.type !== 1);
|
||||
const topics = (filterRes || []).map((item: any) => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
partitionIdList: item.partitionIdList,
|
||||
};
|
||||
});
|
||||
setTopicMetaData(topics);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
if (!jobId) {
|
||||
setLoadingTopic(true);
|
||||
drawerVisible &&
|
||||
getReassignmentList(topicSelectValue)
|
||||
.then((res: any[]) => {
|
||||
setTopicData(res);
|
||||
const newReplica = res.map((t) => t.replicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
const needMovePartitions = res.map((t) => t.partitionIdList || []);
|
||||
setNeedMovePartitions(needMovePartitions);
|
||||
const MoveDataTimeRanges = res.map((t) => {
|
||||
const timeHour = t.retentionMs / 1000 / 60 / 60;
|
||||
return timeHour > 1 ? Math.floor(timeHour) : timeHour.toFixed(2);
|
||||
});
|
||||
setMoveDataTimeRanges(MoveDataTimeRanges);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [topicSelectValue, drawerVisible]);
|
||||
|
||||
// ---------- 新增穿梭框替换目标节点select
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
setVisible(true);
|
||||
Utils.request(Api.getDashboardMetadata(routeParams.clusterId, MetricType.Broker)).then((res: any) => {
|
||||
const dataDe = res || [];
|
||||
const dataHandle = dataDe.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
key: item.brokerId,
|
||||
title: `${item.brokerId} (${item.host})`,
|
||||
};
|
||||
});
|
||||
setBrokerList(dataHandle);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
const addReassign = () => {
|
||||
if (selectBrokerList.length && topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
form.validateFields().then((e) => {
|
||||
const formData = form.getFieldsValue();
|
||||
const handledData = {
|
||||
creator: global.userInfo.userName,
|
||||
jobType: 0, // type 0 topic迁移 1 扩缩容 2集群均衡
|
||||
planTime: formData.planTime,
|
||||
jobStatus: jobId ? jobStatus : 2, //status 2 创建
|
||||
target: topicSelectValue.join(','),
|
||||
id: jobId || '',
|
||||
jobDesc: formData.description,
|
||||
jobData: JSON.stringify({
|
||||
clusterId: routeParams.clusterId,
|
||||
jobDesc: formData.description,
|
||||
throttleUnitB: formData.throttle * 1024 * 1024,
|
||||
topicPlanList: topicSelectValue.map((topic, index) => {
|
||||
return {
|
||||
clusterId: routeParams.clusterId,
|
||||
topicName: topic,
|
||||
partitionIdList: needMovePartitions[index],
|
||||
partitionNum: needMovePartitions[index].length,
|
||||
presentReplicaNum: topicNewReplicas[index],
|
||||
newReplicaNum: topicNewReplicas[index] || topic.replicaNum,
|
||||
originalBrokerIdList: taskPlanData[index].currentBrokerIdList,
|
||||
reassignBrokerIdList: taskPlanData[index].reassignBrokerIdList,
|
||||
originalRetentionTimeUnitMs: topicData[index].retentionMs,
|
||||
reassignRetentionTimeUnitMs: moveDataTimeRanges[index] * 60 * 60 * 1000,
|
||||
latestDaysAvgBytesInList: topicData[index].latestDaysAvgBytesInList,
|
||||
latestDaysMaxBytesInList: topicData[index].latestDaysMaxBytesInList,
|
||||
partitionPlanList: taskPlanData[index].partitionPlanList,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
};
|
||||
if (jobId) {
|
||||
Utils.put(Api.putJobsTaskData(routeParams.clusterId), handledData)
|
||||
.then(() => {
|
||||
message.success('迁移任务编辑成功');
|
||||
onDrawerClose();
|
||||
genData();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err, 'err');
|
||||
});
|
||||
} else {
|
||||
Utils.post(Api.createTask(routeParams.clusterId), handledData).then(() => {
|
||||
message.success('迁移任务创建成功');
|
||||
onDrawerClose();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title={jobNameMap[type]}
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onDrawerClose}
|
||||
visible={visible}
|
||||
className="topic-job-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={(_) => {
|
||||
// setVisible(false);
|
||||
onDrawerClose();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={addReassign}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="wrap">
|
||||
<h4 className="title">{jobNameMap[type]}Topic</h4>
|
||||
|
||||
{!jobId && (
|
||||
<Form form={form}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item>
|
||||
<Select
|
||||
placeholder="请选择Topic,可多选"
|
||||
mode="multiple"
|
||||
onChange={(v: any) => {
|
||||
setTopicSelectValue(v);
|
||||
}}
|
||||
options={topicMetaData}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<Table dataSource={topicData} columns={topicDataColumns} pagination={false} loading={loadingTopic} />
|
||||
<Form form={form} layout="vertical" className="task-form">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="brokerList" label="目标节点" rules={[{ required: true }, { validator: checkRep }]}>
|
||||
<Transfer
|
||||
dataSource={brokerList}
|
||||
showSearch
|
||||
filterOption={(inputValue, option) => option.host.indexOf(inputValue) > -1}
|
||||
targetKeys={selectBrokerList}
|
||||
onChange={nodeChange}
|
||||
render={(item) => item.title}
|
||||
titles={['待选节点', '已选节点']}
|
||||
customHeader
|
||||
showSelectedCount
|
||||
locale={{ itemUnit: '', itemsUnit: '' }}
|
||||
suffix={<IconFont type="icon-fangdajing" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{/* taskPlanData是传给组件的初始值
|
||||
点击预览任务计划,触发onClickPreview回调,发起请求获取taskPlanData
|
||||
组件内部改完点击每一行保存时,再通过onClickSavePreview回调向外派发数据 */}
|
||||
<PreviewTaskPlan
|
||||
taskPlanData={taskPlanData}
|
||||
onClickPreview={onClickPreview}
|
||||
onClickSavePreview={onClickSavePreview}
|
||||
brokerList={brokerList}
|
||||
></PreviewTaskPlan>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4 className="title">迁移任务配置</h4>
|
||||
<Row gutter={32} className="topic-execution-time">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="throttle"
|
||||
label="限流"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (r: any, v: number) => {
|
||||
if ((v || v === 0) && v <= 0) {
|
||||
return Promise.reject('限流值不能小于或等于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
// formatter={(value) => `${value} MB/s`}
|
||||
// parser={(value) => value.replace('MB/s', '')}
|
||||
addonAfter="MB/S"
|
||||
max={99999}
|
||||
></InputNumber>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="planTime" label="任务执行时间" rules={[{ required: true }]}>
|
||||
<DatePicker showTime style={{ width: '100%' }} disabledDate={disabledDate} disabledTime={disabledDateTime} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4 className="title">描述</h4>
|
||||
<Form.Item name="description" label="任务描述" rules={[{ required: true }]}>
|
||||
<TextArea placeholder="暂支持 String 格式" style={{ height: 110 }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
.topic-job-drawer {
|
||||
.divider {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: #CED4DA;
|
||||
}
|
||||
|
||||
.dcloud-drawer-body {
|
||||
// padding: 0 20px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
.title {
|
||||
font-size: 16px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
line-height: 25px;
|
||||
margin: 16px 0;
|
||||
font-family: @font-family-bold;
|
||||
}
|
||||
|
||||
.topic-execution-time{
|
||||
.dcloud-form-item-has-error{
|
||||
.dcloud-picker{
|
||||
border-color: #ff7066 !important;
|
||||
background: #fffafa !important;
|
||||
}
|
||||
}
|
||||
.dcloud-picker{
|
||||
background-color: rgba(33, 37, 41, 0.06);
|
||||
border-color: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dcloud-picker-focused,.dcloud-picker:hover{
|
||||
border-color: #74788d;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-tag-wrap {
|
||||
display: flex;
|
||||
|
||||
.custom-tag {
|
||||
padding: 4px;
|
||||
background: #ECECF6;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: #495057;
|
||||
margin-right: 4px;
|
||||
|
||||
.time {
|
||||
color: #74788D;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.dcloud-select-selector {
|
||||
max-height: 100px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-task-plan-drawer {
|
||||
.partition-task-plan-list {
|
||||
padding: 12px 16px;
|
||||
|
||||
.dcloud-table .dcloud-table-content .dcloud-table-thead .dcloud-table-cell,
|
||||
.dcloud-table .dcloud-table-content .dcloud-table-tbody .dcloud-table-cell {
|
||||
background: #F8F9FA;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user