mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-12 19:12:48 +08:00
初始化3.0.0版本
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import { AppContainer, Form, message, Tabs, Utils } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import ConfigForm from './component/ConfigFrom';
|
||||
import TestResult from '../TestingConsumer/component/Result';
|
||||
import API from '../../api';
|
||||
import { getFormConfig, getTableColumns, tabsConfig } from './config';
|
||||
import './index.less';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const ProduceClientTest = () => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [form] = Form.useForm();
|
||||
const customFormRef: any = React.createRef();
|
||||
const [configInfo, setConfigInfo] = React.useState({});
|
||||
const [activeKey, setActiveKey] = React.useState(tabsConfig[0].name);
|
||||
const [tableData, setTableData] = React.useState([]);
|
||||
const [topicMetaData, setTopicMetaData] = React.useState([]);
|
||||
const [running, setRunning] = React.useState(false);
|
||||
const [isKeyOn, setIsKeyOn] = React.useState(true);
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
|
||||
const currentInterval = React.useRef(null);
|
||||
|
||||
let currentMsgNum = 0;
|
||||
let startTime = 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
Utils.request(API.getTopicMetaData(+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);
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearInterval();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const runProduceClient = (values: any, isFirst = false) => {
|
||||
// 记录每一次请求发送的条数
|
||||
currentMsgNum = currentMsgNum ? currentMsgNum + values.chunks : values.chunks;
|
||||
|
||||
const params = {
|
||||
clientProperties: {
|
||||
acks: values.acks,
|
||||
'compression.type': values.compressionType,
|
||||
},
|
||||
clusterId,
|
||||
partitionIdList: values.frocePartition,
|
||||
recordCount: values.chunks,
|
||||
recordHeader: values.recordHeader,
|
||||
recordKey: isKeyOn ? values.key || '' : undefined,
|
||||
recordOperate: isFirst,
|
||||
recordValue: values.value,
|
||||
topicName: values.topicName,
|
||||
};
|
||||
|
||||
Utils.post(API.postClientProducer, params)
|
||||
.then((res: any) => {
|
||||
if (isFirst) {
|
||||
startTime = new Date().getTime();
|
||||
}
|
||||
setTableData(res || []);
|
||||
if (values.producerMode === 'timed') {
|
||||
if (values.interval && !currentInterval.current) {
|
||||
// const randomValue = Math.random() < 0.5 ? -1 : 1;
|
||||
// const random = +values.jitter ? +values.interval + randomValue * (Math.random() * +values.jitter) : values.interval;
|
||||
const random = values.interval;
|
||||
|
||||
currentInterval.current = window.setInterval(() => {
|
||||
runProduceClient(values);
|
||||
}, random);
|
||||
}
|
||||
// 第一次接口返回到现在的时间大于运行时间 当前发送条数大于总条数停止请求
|
||||
const currentTime = new Date().getTime();
|
||||
// if (currentTime - startTime >= +values.elapsed || currentMsgNum >= values.messageProduced) {
|
||||
if (currentTime - startTime >= +values.elapsed) {
|
||||
clearInterval();
|
||||
setRunning(false);
|
||||
}
|
||||
} else {
|
||||
// manual 方式
|
||||
setRunning(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
// manual 方式
|
||||
if (values.producerMode !== 'timed') {
|
||||
setRunning(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
values.elapsed = values.elapsed * 60 * 1000;
|
||||
const data = customFormRef.current.getTableData();
|
||||
values.recordHeader = {};
|
||||
for (const item of data) {
|
||||
values.recordHeader[item.key] = item.value;
|
||||
}
|
||||
// 点击按钮重新定时器清空
|
||||
clearInterval();
|
||||
setRunning(true);
|
||||
runProduceClient(values, true);
|
||||
})
|
||||
.catch((error) => {
|
||||
const { values } = error;
|
||||
if (!values.chunks || (values.producerMode === 'timed' && (!values.interval || !values.elapsed))) {
|
||||
setActiveKey('Flow');
|
||||
}
|
||||
if (!values.topicName || !values.value) {
|
||||
setActiveKey('Data');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearInterval = () => {
|
||||
currentInterval.current && window.clearInterval(currentInterval.current);
|
||||
currentInterval.current = null;
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
currentInterval.current && window.clearInterval(currentInterval.current);
|
||||
setRunning(false);
|
||||
};
|
||||
|
||||
const onHandleValuesChange = (value: any, allValues: any) => {
|
||||
Object.keys(value).forEach((key) => {
|
||||
let changeValue: any = null;
|
||||
let partitionIdList = [];
|
||||
switch (key) {
|
||||
case 'producerMode':
|
||||
changeValue = value[key];
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
needTimeOption: changeValue === 'timed',
|
||||
});
|
||||
break;
|
||||
case 'interval':
|
||||
changeValue = value[key];
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
maxJitter: +value[key] - 1,
|
||||
});
|
||||
break;
|
||||
case 'topicName':
|
||||
changeValue = value[key] as string;
|
||||
AppContainer.eventBus.emit('ProduceTopicChange', value[key]);
|
||||
partitionIdList = topicMetaData.find((item) => item.label === changeValue)?.partitionIdList || [];
|
||||
partitionIdList = partitionIdList.map((item: number) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}));
|
||||
setConfigInfo({
|
||||
...configInfo,
|
||||
partitionIdList,
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clearForm = () => {
|
||||
setConfigInfo({});
|
||||
clearInterval();
|
||||
setTableData([]);
|
||||
form.resetFields();
|
||||
AppContainer.eventBus.emit('ProduceTopicChange', '');
|
||||
setIsKeyOn(true);
|
||||
customFormRef.current.resetTable();
|
||||
};
|
||||
|
||||
const onTabChange = (key: string) => {
|
||||
setActiveKey(key);
|
||||
};
|
||||
|
||||
const onKeySwitchChange = (checked: boolean) => {
|
||||
setIsKeyOn(checked);
|
||||
};
|
||||
|
||||
const TabsContent = (
|
||||
<div className="form-tabs">
|
||||
<Tabs defaultActiveKey={tabsConfig[0].name} activeKey={activeKey} onChange={onTabChange}>
|
||||
{tabsConfig.map(({ name, control }) =>
|
||||
!control || (global.isShowControl && global.isShowControl(control)) ? <TabPane tab={name} key={name} /> : <></>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="client-test-panel">
|
||||
<ConfigForm
|
||||
title="生产配置"
|
||||
customContent={TabsContent}
|
||||
customFormRef={customFormRef}
|
||||
activeKey={activeKey}
|
||||
formConfig={getFormConfig({
|
||||
topicMetaData,
|
||||
activeKey,
|
||||
configInfo,
|
||||
form,
|
||||
onKeySwitchChange,
|
||||
isKeyOn,
|
||||
isShowControl: global.isShowControl,
|
||||
})}
|
||||
formData={{}}
|
||||
form={form}
|
||||
onHandleValuesChange={onHandleValuesChange}
|
||||
clearForm={clearForm}
|
||||
submit={run}
|
||||
running={running}
|
||||
stop={stop}
|
||||
/>
|
||||
<TestResult
|
||||
showProcessList={false}
|
||||
tableProps={{
|
||||
scroll: { y: 600 },
|
||||
columns: getTableColumns(),
|
||||
dataSource: tableData,
|
||||
pagination: false,
|
||||
}}
|
||||
showCardList={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProduceClientTest;
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Button, Col, Form, Row } from 'knowdesign';
|
||||
import { FormItemType, handleFormItem, IFormItem, renderFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
import * as React from 'react';
|
||||
import './style/form.less';
|
||||
import EditTable from './EditTable';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const prefixTesting = 'config-form-panel';
|
||||
interface IProps {
|
||||
formConfig: IFormItem[];
|
||||
formData: any;
|
||||
form: any;
|
||||
customFormRef: any;
|
||||
onHandleValuesChange?: any;
|
||||
title: string;
|
||||
customContent?: React.ReactNode;
|
||||
customForm?: React.ReactNode;
|
||||
clearForm: any;
|
||||
submit: any;
|
||||
stop: any;
|
||||
activeKey: string;
|
||||
running?: boolean;
|
||||
}
|
||||
|
||||
export const renderFormContent = ({ formMap, formData, layout, formLayout, formItemColSpan = 24 }: any) => {
|
||||
return (
|
||||
<Row gutter={10}>
|
||||
{formMap.map((formItem: IFormItem) => {
|
||||
const { initialValue = undefined, valuePropName } = handleFormItem(formItem, formData);
|
||||
if (formItem.type === FormItemType.text)
|
||||
return (
|
||||
<Col style={{ display: formItem.invisible ? 'none' : '' }} key={formItem.key} span={formItem.colSpan || formItemColSpan}>
|
||||
{layout === 'vertical' ? (
|
||||
<>
|
||||
<span className="dcloud-form-item-label" style={{ padding: '0 0 7px', display: 'block' }}>
|
||||
{formItem.label}
|
||||
</span>
|
||||
{(formItem as any).customFormItem ? (
|
||||
<span style={{ fontSize: '14px', padding: '0 0 16px', display: 'block' }}>{(formItem as any).customFormItem}</span>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Row style={{ padding: '6px 0 10px' }}>
|
||||
<Col span={formLayout?.labelCol.span || 4} style={{ textAlign: 'right' }}>
|
||||
<span className="dcloud-form-item-label" style={{ padding: '0 10px 0 0', display: 'inline-block' }}>
|
||||
{formItem.label}:
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={formLayout?.wrapperCol.span || 20}>
|
||||
<span>{(formItem as any).customFormItem}</span>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
return (
|
||||
<Col style={{ display: formItem.invisible ? 'none' : '' }} key={formItem.key} span={formItem.colSpan || formItemColSpan}>
|
||||
<Form.Item
|
||||
name={formItem.key}
|
||||
key={formItem.key}
|
||||
label={formItem.label}
|
||||
rules={formItem.rules || [{ required: false, message: '' }]}
|
||||
initialValue={initialValue}
|
||||
valuePropName={valuePropName}
|
||||
{...formItem.formAttrs}
|
||||
>
|
||||
{renderFormItem(formItem)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const ConfigForm = (props: IProps): JSX.Element => {
|
||||
const {
|
||||
formConfig,
|
||||
activeKey,
|
||||
customFormRef,
|
||||
form,
|
||||
formData,
|
||||
onHandleValuesChange,
|
||||
title,
|
||||
clearForm,
|
||||
submit,
|
||||
stop,
|
||||
running,
|
||||
customContent,
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
|
||||
const onSubmit = () => {
|
||||
running ? stop() : submit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={prefixTesting}>
|
||||
<div className={`${prefixTesting}-title`}>{title}</div>
|
||||
<div>{customContent}</div>
|
||||
<div className={`${prefixTesting}-content`}>
|
||||
<div style={{ display: activeKey === 'Header' ? '' : 'none' }}>
|
||||
<EditTable ref={customFormRef} />
|
||||
</div>
|
||||
<Form form={form} layout={'vertical'} onValuesChange={onHandleValuesChange}>
|
||||
{renderFormContent({ formMap: formConfig, formData, layout: 'vertical' })}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className={`${prefixTesting}-footer-btn`}>
|
||||
<Button loading={running} onClick={clearForm}>
|
||||
{intl.formatMessage({ id: 'test.client.clear' })}
|
||||
</Button>
|
||||
<Button onClick={onSubmit} type="primary">
|
||||
{intl.formatMessage({ id: running ? 'test.client.stop' : 'test.client.run' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigForm;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PicLeftOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Tooltip } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
|
||||
interface IProps {
|
||||
value?: string;
|
||||
onChange?: any;
|
||||
tooltip?: string;
|
||||
}
|
||||
const CustomTextArea = (props: IProps) => {
|
||||
const { tooltip, value, onChange } = props;
|
||||
|
||||
const genRandomContent = () => {
|
||||
onChange && onChange(value.trim());
|
||||
};
|
||||
|
||||
const onInputChange = ({ target: { value } }: { target: { value: string } }) => {
|
||||
onChange && onChange(value.trim());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span>
|
||||
<Tooltip title={tooltip}>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip title={'生成随机内容'}>
|
||||
<PicLeftOutlined onClick={genRandomContent} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<Input.TextArea value={value} onChange={onInputChange} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomTextArea;
|
||||
@@ -0,0 +1,217 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState } from 'react';
|
||||
import { Table, Input, InputNumber, Popconfirm, Form, Typography, Button, message, IconFont } from 'knowdesign';
|
||||
import './style/edit-table.less';
|
||||
import { CheckOutlined, CloseOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||
|
||||
const EditableCell = ({ editing, dataIndex, title, inputType, placeholder, record, index, children, ...restProps }: any) => {
|
||||
const inputNode =
|
||||
inputType === 'number' ? (
|
||||
<InputNumber style={{ width: '130px' }} autoComplete="off" placeholder={placeholder} />
|
||||
) : (
|
||||
<Input autoComplete="off" placeholder={placeholder} />
|
||||
);
|
||||
return (
|
||||
<td {...restProps}>
|
||||
{editing ? (
|
||||
<Form.Item
|
||||
name={dataIndex}
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `请输入!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{inputNode}
|
||||
</Form.Item>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
const EditTable = React.forwardRef((props: any, ref: any) => {
|
||||
const { colCustomConfigs } = props;
|
||||
const [form] = Form.useForm();
|
||||
const [data, setData] = useState([]);
|
||||
const [editingKey, setEditingKey] = useState(0);
|
||||
|
||||
const isEditing = (record: any) => record.editKey === editingKey;
|
||||
|
||||
const getTableData = () => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const resetTable = () => {
|
||||
setData([]);
|
||||
};
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getTableData,
|
||||
resetTable,
|
||||
}));
|
||||
|
||||
const edit = (record: any) => {
|
||||
form.setFieldsValue({
|
||||
key: '',
|
||||
value: '',
|
||||
...record,
|
||||
});
|
||||
setEditingKey(record.editKey);
|
||||
};
|
||||
const deleteRow = (record: any) => {
|
||||
const _data = data.filter((item) => item.editKey !== record.editKey);
|
||||
setData(_data);
|
||||
};
|
||||
|
||||
const cancel = (record: any) => {
|
||||
if (record.key === '') {
|
||||
const _data = data.filter((item) => item.editKey !== record.editKey);
|
||||
setData(_data);
|
||||
}
|
||||
setEditingKey(0);
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
if (editingKey !== 0) {
|
||||
message.error('请先保存当前编辑项');
|
||||
return;
|
||||
}
|
||||
form.resetFields();
|
||||
const editKey = data.length ? data[data.length - 1].editKey + 1 : 1;
|
||||
setData([...data, { key: '', value: '', editKey }]);
|
||||
setEditingKey(editKey);
|
||||
};
|
||||
const save = async (editKey: number) => {
|
||||
try {
|
||||
const row = await form.validateFields();
|
||||
const newData = [...data];
|
||||
const index = newData.findIndex((item) => editKey === item.editKey);
|
||||
|
||||
if (index > -1) {
|
||||
const item = newData[index];
|
||||
newData.splice(index, 1, { ...item, ...row });
|
||||
setData(newData);
|
||||
setEditingKey(0);
|
||||
} else {
|
||||
newData.push(row);
|
||||
setData(newData);
|
||||
setEditingKey(0);
|
||||
}
|
||||
} catch (errInfo) {
|
||||
console.log('Validate Failed:', errInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'key',
|
||||
dataIndex: 'key',
|
||||
width: 40,
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
title: 'value',
|
||||
dataIndex: 'value',
|
||||
width: 40,
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
width: 20,
|
||||
className: 'no-padding',
|
||||
render: (text: any, record: any) => {
|
||||
const editable = isEditing(record);
|
||||
return editable ? (
|
||||
<span>
|
||||
<Typography.Link
|
||||
onClick={() => save(record.editKey)}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<CheckOutlined />
|
||||
</Typography.Link>
|
||||
{/* <Popconfirm title="确认取消?" onConfirm={() => cancel(record)}> */}
|
||||
<a>
|
||||
<CloseOutlined onClick={() => cancel(record)} />
|
||||
</a>
|
||||
{/* </Popconfirm> */}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Link
|
||||
className="custom-typography"
|
||||
disabled={editingKey !== 0}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
onClick={() => edit(record)}
|
||||
>
|
||||
<IconFont type={'icon-bianji1'} size={11} />
|
||||
</Typography.Link>
|
||||
<Typography.Link className="custom-typography" disabled={editingKey !== 0} onClick={() => deleteRow(record)}>
|
||||
<IconFont type={'icon-shanchu1'} size={11} />
|
||||
</Typography.Link>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mergedColumns = columns.map((col, index) => {
|
||||
if (!col.editable) {
|
||||
return col;
|
||||
}
|
||||
|
||||
return {
|
||||
...col,
|
||||
onCell: (record: any) =>
|
||||
Object.assign(
|
||||
{
|
||||
record,
|
||||
inputType: 'text',
|
||||
dataIndex: col.dataIndex,
|
||||
title: col.title,
|
||||
editing: isEditing(record),
|
||||
},
|
||||
colCustomConfigs?.[index]
|
||||
),
|
||||
title: colCustomConfigs?.[index]?.title || col.title,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="edit-table-form">
|
||||
<Form form={form} component={false}>
|
||||
<Table
|
||||
components={{
|
||||
body: {
|
||||
cell: EditableCell,
|
||||
},
|
||||
}}
|
||||
// scroll={{
|
||||
// x: true,
|
||||
// }}
|
||||
dataSource={data}
|
||||
columns={mergedColumns}
|
||||
rowClassName="editable-row"
|
||||
pagination={false}
|
||||
/>
|
||||
</Form>
|
||||
<div className="add-btn">
|
||||
<Button type="link" onClick={add}>
|
||||
<PlusSquareOutlined /> 新增条件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default EditTable;
|
||||
@@ -0,0 +1,42 @@
|
||||
.edit-table-form {
|
||||
.dcloud-table-thead>tr>th:not(:last-child):not(.dcloud-table-selection-column):not(.dcloud-table-row-expand-icon-cell):not([colspan])::before {
|
||||
width: 0px;
|
||||
}
|
||||
table {
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
.dcloud-table .dcloud-table-content .dcloud-table-cell {
|
||||
padding: 4px 12px;
|
||||
height: 36px;
|
||||
font-family: HelveticaNeue-Bold;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
|
||||
.dcloud-form-item-control-input {
|
||||
min-height: 27px;
|
||||
}
|
||||
|
||||
.dcloud-input {
|
||||
padding: 4px 12px;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.custom-typography {
|
||||
color: #74788D;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
border-bottom: 1px solid #EFF2F7;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
.config-form-panel {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
min-height: 260px;
|
||||
margin-right: 12px;
|
||||
padding: 16px 24px 60px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 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);
|
||||
border-radius: 12px 12px 0 0;
|
||||
position: relative;
|
||||
height: calc(100vh - 156px);
|
||||
.dcloud-radio-wrapper.dcloud-radio-wrapper-checked {
|
||||
.dcloud-radio-inner {
|
||||
background-color: #526ecc;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-radio-inner {
|
||||
border-color: #ADB5BD;
|
||||
}
|
||||
|
||||
.dcloud-radio-inner::after {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
&-title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&-content{
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 326px);
|
||||
min-height: 90px;
|
||||
}
|
||||
|
||||
&-footer-btn {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
width: 240px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #EBEDEF;
|
||||
padding-top: 5px;
|
||||
|
||||
button+button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { IconFont, Switch, Tooltip } from 'knowdesign';
|
||||
import { FormItemType, IFormItem } from 'knowdesign/lib/extend/x-form';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import { getRandomStr } from '../../lib/utils';
|
||||
import { ControlStatusMap } from '../CommonRoute';
|
||||
|
||||
export const filterList = [
|
||||
{
|
||||
label: 'none',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'contains',
|
||||
value: 'contains',
|
||||
},
|
||||
{
|
||||
label: 'does not contains',
|
||||
value: 'does not contains',
|
||||
},
|
||||
{
|
||||
label: 'equals',
|
||||
value: 'equals',
|
||||
},
|
||||
{
|
||||
label: 'Above Size',
|
||||
value: 'aboveSize',
|
||||
},
|
||||
{
|
||||
label: 'Under Size',
|
||||
value: 'underSize',
|
||||
},
|
||||
];
|
||||
|
||||
export const untilList = [
|
||||
{
|
||||
label: 'Forever',
|
||||
value: 'forever',
|
||||
},
|
||||
{
|
||||
label: 'number of messages',
|
||||
value: 'number of messages',
|
||||
},
|
||||
{
|
||||
label: 'number of messages per partition',
|
||||
value: 'number of messages per partition',
|
||||
},
|
||||
{
|
||||
label: 'max size',
|
||||
value: 'max size',
|
||||
},
|
||||
{
|
||||
label: 'max size per partition',
|
||||
value: 'max size per partition',
|
||||
},
|
||||
{
|
||||
label: 'timestamp',
|
||||
value: 'timestamp',
|
||||
},
|
||||
];
|
||||
|
||||
export const tabsConfig = [
|
||||
{ name: 'Data', control: '' },
|
||||
{ name: 'Flow', control: '' },
|
||||
{ name: 'Header', control: ControlStatusMap.TESTING_PRODUCER_HEADER },
|
||||
{ name: 'Options', control: '' },
|
||||
];
|
||||
|
||||
const getComplexLabel = (key: string, label: string, form: any, onSwitchChange?: any, needSwitch = false) => (
|
||||
<div className="complex-label">
|
||||
<span>
|
||||
<span>{label}</span>
|
||||
<Tooltip title="暂支持string格式">
|
||||
<QuestionCircleOutlined size={12} style={{ marginLeft: 2 }} />
|
||||
</Tooltip>
|
||||
:
|
||||
{needSwitch ? (
|
||||
<span>
|
||||
<Switch onClick={onSwitchChange} size="small" defaultChecked className="switch" />
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip title={'生成随机内容'}>
|
||||
<IconFont
|
||||
type="icon-shengchengdaima"
|
||||
className="random-icon"
|
||||
onClick={() => {
|
||||
const randomStr = getRandomStr(key === 'key' ? 30 : 128);
|
||||
form && form.setFieldsValue({ [key]: randomStr });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const getFormConfig = (params: any) => {
|
||||
const { topicMetaData, activeKey: type, configInfo: info, form, onKeySwitchChange, isKeyOn, isShowControl } = params;
|
||||
const formConfig = [
|
||||
{
|
||||
key: 'topicName',
|
||||
label: 'Topic',
|
||||
type: FormItemType.select,
|
||||
invisible: type !== 'Data',
|
||||
rules: [{ required: true, message: '请选择Topic' }],
|
||||
options: topicMetaData,
|
||||
attrs: {
|
||||
showSearch: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
label: getComplexLabel('key', 'Key', form, onKeySwitchChange, true),
|
||||
type: FormItemType.textArea,
|
||||
invisible: type !== 'Data',
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
message: '请输入Key',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
disabled: !isKeyOn,
|
||||
rows: 5,
|
||||
placeholder: '暂支持string类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
label: getComplexLabel('value', 'Value', form),
|
||||
type: FormItemType.textArea,
|
||||
invisible: type !== 'Data',
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Value',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
rows: 5,
|
||||
placeholder: '暂支持string类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'chunks',
|
||||
label: '单次发送消息数',
|
||||
type: FormItemType.inputNumber,
|
||||
invisible: type !== 'Flow',
|
||||
defaultValue: 1,
|
||||
rules: [{ required: true, message: '请输入' }],
|
||||
attrs: {
|
||||
min: 0,
|
||||
style: { width: 232 },
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'producerMode',
|
||||
label: '生产模式',
|
||||
type: FormItemType.radioGroup,
|
||||
invisible: type !== 'Flow',
|
||||
defaultValue: 'manual',
|
||||
options: [
|
||||
{
|
||||
label: '手动',
|
||||
value: 'manual',
|
||||
},
|
||||
{
|
||||
label: '周期',
|
||||
value: 'timed',
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择生产模式',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// key: 'timeOptions',
|
||||
// label: 'Timer options',
|
||||
// type: FormItemType.text,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// customFormItem: null,
|
||||
// rules: [
|
||||
// {
|
||||
// required: false,
|
||||
// message: '请选择Producer Mode',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
key: 'elapsed',
|
||||
label: '运行总时间(min)',
|
||||
type: FormItemType.inputNumber,
|
||||
invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needTimeOption,
|
||||
message: '请输入',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 300,
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'interval',
|
||||
label: '时间间隔(ms)',
|
||||
type: FormItemType.inputNumber,
|
||||
invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
rules: [
|
||||
{
|
||||
required: info?.needTimeOption,
|
||||
message: '请输入',
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
min: 300, // 300ms间隔保证请求不太频繁
|
||||
style: { width: '100%' },
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: 'jitter',
|
||||
// label: 'Max jitter(ms)',
|
||||
// type: FormItemType.inputNumber,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// rules: [
|
||||
// {
|
||||
// required: info?.needTimeOption,
|
||||
// message: '请输入Max jitter',
|
||||
// },
|
||||
// ],
|
||||
// attrs: {
|
||||
// max: info.maxJitter || 0,
|
||||
// size: 'small',
|
||||
// style: { width: 216 },
|
||||
// },
|
||||
// formAttrs: {
|
||||
// className: 'inner-item',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// key: 'lifecycle',
|
||||
// label: (
|
||||
// <>
|
||||
// Lifecycle options
|
||||
// <Tooltip title="Shutdown the producer automatically">
|
||||
// <QuestionCircleOutlined style={{ marginLeft: 8 }} />
|
||||
// </Tooltip>
|
||||
// </>
|
||||
// ),
|
||||
// type: FormItemType.text,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// customFormItem: null,
|
||||
// rules: [
|
||||
// {
|
||||
// required: false,
|
||||
// message: '',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// key: 'messageProduced',
|
||||
// label: 'Number of message produced',
|
||||
// type: FormItemType.inputNumber,
|
||||
// invisible: type !== 'Flow' || !info?.needTimeOption,
|
||||
// rules: [
|
||||
// {
|
||||
// required: info?.needTimeOption,
|
||||
// message: '请输入Number of message produced',
|
||||
// },
|
||||
// ],
|
||||
// attrs: {
|
||||
// min: 0,
|
||||
// size: 'small',
|
||||
// style: { width: 216 },
|
||||
// },
|
||||
// formAttrs: {
|
||||
// className: 'inner-item',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
key: 'frocePartition',
|
||||
label: 'Froce Partition',
|
||||
invisible: type !== 'Options',
|
||||
type: FormItemType.select,
|
||||
attrs: {
|
||||
mode: 'multiple',
|
||||
},
|
||||
options: info.partitionIdList || [],
|
||||
rules: [{ required: false, message: '请选择' }],
|
||||
},
|
||||
{
|
||||
key: 'compressionType',
|
||||
label: 'Compression Type',
|
||||
type: FormItemType.radioGroup,
|
||||
defaultValue: 'none',
|
||||
invisible: type !== 'Options',
|
||||
options: (() => {
|
||||
const options = [
|
||||
{
|
||||
label: 'none',
|
||||
value: 'none',
|
||||
},
|
||||
{
|
||||
label: 'gzip',
|
||||
value: 'gzip',
|
||||
},
|
||||
{
|
||||
label: 'snappy',
|
||||
value: 'snappy',
|
||||
},
|
||||
{
|
||||
label: 'lz4',
|
||||
value: 'lz4',
|
||||
},
|
||||
];
|
||||
|
||||
if (isShowControl && isShowControl(ControlStatusMap.TESTING_PRODUCER_COMPRESSION_TYPE_ZSTD)) {
|
||||
options.push({
|
||||
label: 'zstd',
|
||||
value: 'zstd',
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
})(),
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
message: '请选择Compression Type',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'acks',
|
||||
label: 'Acks',
|
||||
type: FormItemType.radioGroup,
|
||||
defaultValue: '0',
|
||||
invisible: type !== 'Options',
|
||||
options: [
|
||||
{
|
||||
label: 'none',
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
label: 'leader',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: 'all',
|
||||
value: 'all',
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
required: false,
|
||||
message: '请选择Acks',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as unknown as IFormItem[];
|
||||
|
||||
return formConfig;
|
||||
};
|
||||
|
||||
export const getTableColumns = () => {
|
||||
return [
|
||||
{
|
||||
title: 'Partition',
|
||||
dataIndex: 'partitionId',
|
||||
},
|
||||
{
|
||||
title: 'offset',
|
||||
dataIndex: 'offset',
|
||||
},
|
||||
{
|
||||
title: 'Timestamp',
|
||||
dataIndex: 'timestampUnitMs',
|
||||
render: (text: number) => {
|
||||
return moment(text).format(timeFormat);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'time',
|
||||
dataIndex: 'costTimeUnitMs',
|
||||
width: 60,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
.client-test-panel {
|
||||
display: flex;
|
||||
|
||||
.form-tabs {
|
||||
background: transparent;
|
||||
|
||||
.dcloud-tabs-nav {
|
||||
height: 32px;
|
||||
background: none;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-top>.dcloud-tabs-nav,
|
||||
.dcloud-tabs-bottom>.dcloud-tabs-nav,
|
||||
.dcloud-tabs-top>div>.dcloud-tabs-nav,
|
||||
.dcloud-tabs-bottom>div>.dcloud-tabs-nav {
|
||||
margin-bottom: 18px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-tab+.dcloud-tabs-tab {
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
|
||||
.dcloud-tabs-top>.dcloud-tabs-nav::before,
|
||||
.dcloud-tabs-bottom>.dcloud-tabs-nav::before,
|
||||
.dcloud-tabs-top>div>.dcloud-tabs-nav::before,
|
||||
.dcloud-tabs-bottom>div>.dcloud-tabs-nav::before {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item {
|
||||
margin-bottom: 14px;
|
||||
|
||||
&-label {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
line-height: 18px !important;
|
||||
|
||||
.complex-label {
|
||||
width: 228px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.random-icon {
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
color: #ADB5BC;
|
||||
}
|
||||
|
||||
.random-icon:hover {
|
||||
height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 9px;
|
||||
line-height: 20px;
|
||||
color: #74788D;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-form-item-control-input {
|
||||
textarea {
|
||||
height: 110px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dcloud-form-item.inner-item {
|
||||
margin-left: 16px;
|
||||
margin-bottom: 7px;
|
||||
|
||||
.dcloud-form-item-label {
|
||||
padding: 0 0 4px;
|
||||
|
||||
>label {
|
||||
font-size: 13px;
|
||||
color: #74788D;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dcloud-radio-group{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.dcloud-radio-wrapper{
|
||||
width: 33.33%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AppContainer } from 'knowdesign';
|
||||
import * as React from 'react';
|
||||
import ProduceClientTest from './Produce';
|
||||
import './index.less';
|
||||
import TaskTabs from '../TestingConsumer/component/TaskTabs';
|
||||
import DBreadcrumb from 'knowdesign/lib/extend/d-breadcrumb';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const Produce = () => {
|
||||
const initial = {
|
||||
label: '生产',
|
||||
key: 'tab-1',
|
||||
closable: false,
|
||||
tabpane: <ProduceClientTest />,
|
||||
};
|
||||
const { clusterId } = useParams<{ clusterId: string }>();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const ref: any = React.useRef();
|
||||
|
||||
React.useEffect(() => {
|
||||
AppContainer.eventBus.on('ProduceTopicChange', (args: string) => {
|
||||
ref.current && ref.current.setTabsTitle && ref.current.setTabsTitle(`生产 ${args}`);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="breadcrumb">
|
||||
<DBreadcrumb
|
||||
breadcrumbs={[
|
||||
{ label: '多集群管理', aHref: '/' },
|
||||
{ label: global?.clusterInfo?.name, aHref: `/cluster/${global?.clusterInfo?.id}` },
|
||||
{ label: 'Produce', aHref: '' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<TaskTabs initial={initial} ref={ref} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Produce;
|
||||
Reference in New Issue
Block a user