mirror of
https://github.com/didi/KnowStreaming.git
synced 2026-01-03 02:52:08 +08:00
kafka-manager 2.0
This commit is contained in:
139
kafka-manager-console/src/component/antd/index.tsx
Normal file
139
kafka-manager-console/src/component/antd/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import message from 'antd/es/message';
|
||||
import 'antd/es/message/style';
|
||||
|
||||
import Input from 'antd/es/input';
|
||||
import 'antd/es/input/style';
|
||||
|
||||
import InputNumber from 'antd/es/input-number';
|
||||
import 'antd/es/input-number/style';
|
||||
|
||||
import Table from 'antd/es/table';
|
||||
import 'antd/es/table/style';
|
||||
|
||||
import Tabs from 'antd/es/tabs';
|
||||
import 'antd/es/tabs/style';
|
||||
|
||||
import Select from 'antd/es/select';
|
||||
import 'antd/es/select/style';
|
||||
|
||||
import DatePicker from 'antd/es/date-picker';
|
||||
import 'antd/es/date-picker/style';
|
||||
|
||||
import Button from 'antd/es/button';
|
||||
import 'antd/es/button/style';
|
||||
|
||||
import Modal from 'antd/es/modal';
|
||||
import 'antd/es/modal/style';
|
||||
|
||||
import Form from 'antd/es/form';
|
||||
import 'antd/es/form/style';
|
||||
|
||||
import Row from 'antd/es/row';
|
||||
import 'antd/es/row/style';
|
||||
|
||||
import Col from 'antd/es/col';
|
||||
import 'antd/es/col/style';
|
||||
|
||||
import Switch from 'antd/es/switch';
|
||||
import 'antd/es/switch/style';
|
||||
|
||||
import Alert from 'antd/es/alert';
|
||||
import 'antd/es/alert/style';
|
||||
|
||||
import { PaginationConfig, ColumnProps } from 'antd/es/table/interface';
|
||||
|
||||
import notification from 'antd/es/notification';
|
||||
import 'antd/es/notification/style';
|
||||
|
||||
import Tooltip from 'antd/es/tooltip';
|
||||
import 'antd/es/tooltip/style';
|
||||
|
||||
import Radio from 'antd/es/radio';
|
||||
import 'antd/es/radio';
|
||||
import { RadioChangeEvent } from 'antd/es/radio';
|
||||
|
||||
import Collapse from 'antd/es/collapse';
|
||||
import 'antd/es/collapse/style';
|
||||
|
||||
import Icon from 'antd/es/icon';
|
||||
import 'antd/es/icon/style';
|
||||
|
||||
import Dropdown from 'antd/es/dropdown';
|
||||
import 'antd/es/dropdown/style';
|
||||
|
||||
import Spin from 'antd/es/spin';
|
||||
import 'antd/es/spin/style';
|
||||
|
||||
import Drawer from 'antd/es/drawer';
|
||||
import 'antd/es/drawer/style';
|
||||
|
||||
import Checkbox from 'antd/es/checkbox';
|
||||
import 'antd/es/checkbox/style';
|
||||
|
||||
import Affix from 'antd/es/affix';
|
||||
import 'antd/es/affix/style';
|
||||
|
||||
import Popconfirm from 'antd/es/popconfirm';
|
||||
import 'antd/es/popconfirm/style';
|
||||
|
||||
import PageHeader from 'antd/es/page-header';
|
||||
import 'antd/es/page-header/style';
|
||||
|
||||
import Descriptions from 'antd/es/descriptions';
|
||||
import 'antd/es/descriptions/style';
|
||||
|
||||
import Steps from 'antd/es/steps';
|
||||
import 'antd/es/steps/style';
|
||||
|
||||
import Divider from 'antd/es/divider';
|
||||
import 'antd/es/divider/style';
|
||||
|
||||
import Upload from 'antd/es/upload';
|
||||
import 'antd/es/upload/style';
|
||||
|
||||
import TimePicker from 'antd/es/time-picker';
|
||||
import 'antd/es/time-picker/style';
|
||||
|
||||
import Badge from 'antd/es/badge';
|
||||
import 'antd/es/badge/style';
|
||||
|
||||
import { RangePickerValue } from 'antd/es/date-picker/interface';
|
||||
|
||||
export {
|
||||
PaginationConfig,
|
||||
notification,
|
||||
ColumnProps,
|
||||
DatePicker,
|
||||
message,
|
||||
Tooltip,
|
||||
Button,
|
||||
Select,
|
||||
Switch,
|
||||
Modal,
|
||||
Input,
|
||||
Table,
|
||||
Radio,
|
||||
Alert,
|
||||
Tabs,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
RadioChangeEvent,
|
||||
InputNumber,
|
||||
Collapse,
|
||||
Icon,
|
||||
Dropdown,
|
||||
Spin,
|
||||
Drawer,
|
||||
Checkbox,
|
||||
Affix,
|
||||
Popconfirm,
|
||||
PageHeader,
|
||||
Descriptions,
|
||||
Steps,
|
||||
Divider,
|
||||
Upload,
|
||||
TimePicker,
|
||||
RangePickerValue,
|
||||
Badge,
|
||||
};
|
||||
90
kafka-manager-console/src/component/chart/bar-chart.tsx
Normal file
90
kafka-manager-console/src/component/chart/bar-chart.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import * as React from 'react';
|
||||
import { Spin, notification } from 'component/antd';
|
||||
import echarts, { EChartOption } from 'echarts/lib/echarts';
|
||||
|
||||
// 引入柱状图
|
||||
import 'echarts/lib/chart/bar';
|
||||
|
||||
// 引入提示框和标题组件
|
||||
import 'echarts/lib/component/tooltip';
|
||||
import 'echarts/lib/component/title';
|
||||
import 'echarts/lib/component/legend';
|
||||
|
||||
interface IChartProps {
|
||||
getChartData: any;
|
||||
customerNode?: React.ReactNode;
|
||||
}
|
||||
|
||||
export class BarChartComponet extends React.Component<IChartProps> {
|
||||
public id: HTMLDivElement = null;
|
||||
public chart: echarts.ECharts;
|
||||
|
||||
public state = {
|
||||
loading: false,
|
||||
noData: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.chart = echarts.init(this.id);
|
||||
this.getChartData();
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
public resize = () => {
|
||||
this.chart.resize();
|
||||
}
|
||||
|
||||
public isHasData = (data: EChartOption) => {
|
||||
const noData = !(data.series && data.series.length);
|
||||
this.setState({ noData });
|
||||
return !noData;
|
||||
}
|
||||
|
||||
public getChartData = () => {
|
||||
const { getChartData } = this.props;
|
||||
if (!getChartData) {
|
||||
return notification.error({ message: '图表信息有误' });
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
const chartOptions = getChartData();
|
||||
|
||||
if ((typeof chartOptions.then) === 'function') {
|
||||
return chartOptions.then((data: EChartOption) => {
|
||||
this.setState({ loading: false });
|
||||
|
||||
if (this.isHasData(data)) {
|
||||
this.changeChartOptions(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isHasData(chartOptions)) {
|
||||
this.changeChartOptions(chartOptions);
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
public changeChartOptions(options: any) {
|
||||
this.chart.setOption(options, true);
|
||||
}
|
||||
|
||||
public handleRefreshChart() {
|
||||
this.getChartData();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={this.state.loading} className="chart-content">
|
||||
{this.state.noData ? <div className="nothing-style">暂无数据</div> : null}
|
||||
<div className={this.state.noData ? 'chart-no-data' : 'chart'} ref={(id) => this.id = id} />
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
110
kafka-manager-console/src/component/chart/date-picker-chart.tsx
Normal file
110
kafka-manager-console/src/component/chart/date-picker-chart.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as React from 'react';
|
||||
import { DatePicker, notification, Spin } from 'component/antd';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { timeStampStr } from 'constants/strategy';
|
||||
import { disabledDate } from 'lib/utils';
|
||||
import echarts from 'echarts';
|
||||
|
||||
// 引入柱状图和折线图
|
||||
import 'echarts/lib/chart/bar';
|
||||
import 'echarts/lib/chart/line';
|
||||
|
||||
// 引入提示框和标题组件
|
||||
import 'echarts/lib/component/tooltip';
|
||||
import 'echarts/lib/component/title';
|
||||
import 'echarts/lib/component/legend';
|
||||
import './index.less';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
interface IChartProps {
|
||||
getChartData: (startTime: moment.Moment, endTime: moment.Moment) => any;
|
||||
customerNode?: React.ReactNode;
|
||||
}
|
||||
|
||||
export class ChartWithDatePicker extends React.Component<IChartProps> {
|
||||
public state = {
|
||||
startTime: moment().subtract(1, 'hour'),
|
||||
endTime: moment(),
|
||||
loading: false,
|
||||
noData: false,
|
||||
};
|
||||
|
||||
public id: HTMLDivElement = null;
|
||||
public chart: echarts.ECharts;
|
||||
|
||||
public getData = () => {
|
||||
const { startTime, endTime } = this.state;
|
||||
const { getChartData } = this.props;
|
||||
this.setState({ loading: true });
|
||||
getChartData(startTime, endTime).then((data: any) => {
|
||||
this.setState({ loading: false });
|
||||
this.changeChartOptions(data);
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.chart = echarts.init(this.id);
|
||||
this.getData();
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
public resize = () => {
|
||||
this.chart.resize();
|
||||
}
|
||||
|
||||
public changeChartOptions(options: any) {
|
||||
const noData = options.series.length ? false : true;
|
||||
this.setState({ noData });
|
||||
this.chart.setOption(options, true);
|
||||
}
|
||||
|
||||
public handleTimeChange = (dates: Moment[]) => {
|
||||
this.setState({
|
||||
startTime: dates[0],
|
||||
endTime: dates[1],
|
||||
});
|
||||
const { getChartData } = this.props;
|
||||
this.setState({ loading: true });
|
||||
getChartData(dates[0], dates[1]).then((data: any) => {
|
||||
this.setState({ loading: false });
|
||||
this.changeChartOptions(data);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { customerNode } = this.props;
|
||||
return (
|
||||
<div className="status-box" style={{minWidth: '930px'}}>
|
||||
<div className="status-graph">
|
||||
<div className="k-toolbar">
|
||||
{customerNode}
|
||||
</div>
|
||||
<ul className="k-toolbar">
|
||||
<li>
|
||||
<RangePicker
|
||||
ranges={{
|
||||
今日: [moment().startOf('date'), moment()],
|
||||
近一天: [moment().subtract(1, 'day'), moment()],
|
||||
近一周: [moment().subtract(7, 'day'), moment()],
|
||||
}}
|
||||
disabledDate={disabledDate}
|
||||
defaultValue={[moment().subtract(1, 'hour'), moment()]}
|
||||
format={timeStampStr}
|
||||
onChange={this.handleTimeChange}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Spin spinning={this.state.loading} className="chart-content">
|
||||
{this.state.noData ? <div className="nothing-style">暂无数据</div> : null}
|
||||
<div className={this.state.noData ? 'chart-no-data' : 'chart'} ref={(id) => this.id = id} />
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
60
kafka-manager-console/src/component/chart/doughnut-chart.tsx
Normal file
60
kafka-manager-console/src/component/chart/doughnut-chart.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
import { Spin } from 'component/antd';
|
||||
import echarts from 'echarts/lib/echarts';
|
||||
// 引入饼状图
|
||||
import 'echarts/lib/chart/pie';
|
||||
// 引入提示框和标题组件
|
||||
import 'echarts/lib/component/tooltip';
|
||||
import 'echarts/lib/component/title';
|
||||
import 'echarts/lib/component/legend';
|
||||
|
||||
interface IPieProps {
|
||||
getChartData: any;
|
||||
}
|
||||
|
||||
export class DoughnutChart extends React.Component<IPieProps> {
|
||||
public id: HTMLDivElement = null;
|
||||
public chart: echarts.ECharts;
|
||||
|
||||
public state = {
|
||||
loading: true,
|
||||
isNoData: false,
|
||||
};
|
||||
|
||||
public getChartData = () => {
|
||||
const { getChartData } = this.props;
|
||||
|
||||
this.setState({ loading: true });
|
||||
const options = getChartData();
|
||||
if (!options || !options.series || !options.series.length) {
|
||||
this.setState({
|
||||
isNoData: true,
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeChartOptions(options);
|
||||
}
|
||||
|
||||
public changeChartOptions(options: any) {
|
||||
this.chart.setOption(options, true);
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.chart = echarts.init(this.id);
|
||||
this.getChartData();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={this.state.loading} className="chart-content">
|
||||
{this.state.isNoData ? <div className="nothing-style">暂无数据</div> : null}
|
||||
<div className="doughnut-chart" ref={(id) => this.id = id} />
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
80
kafka-manager-console/src/component/chart/index.less
Normal file
80
kafka-manager-console/src/component/chart/index.less
Normal file
@@ -0,0 +1,80 @@
|
||||
.status-box{
|
||||
float: left;
|
||||
margin: 0 5px;
|
||||
width: 100%;
|
||||
.status-graph {
|
||||
position: relative;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 255);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 48px;
|
||||
font-family: PingFangSC-Regular;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
padding: 0 5px;
|
||||
margin: -15px 0;
|
||||
.k-toolbar {
|
||||
&>span.label {
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
li {
|
||||
float: left;
|
||||
vertical-align: middle;
|
||||
line-height: 48px;
|
||||
margin-right: 20px;
|
||||
|
||||
&>span.label {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-toolbar {
|
||||
float: right;
|
||||
right: 30px;
|
||||
|
||||
span:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.graph-none{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.nothing-style {
|
||||
height: 300px;
|
||||
line-height: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 400px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.doughnut-chart {
|
||||
width: 500px;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.chart-no-data {
|
||||
height: 0px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.no-footer {
|
||||
.ant-modal-confirm-btns {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-info {
|
||||
text-align: center;
|
||||
}
|
||||
4
kafka-manager-console/src/component/chart/index.tsx
Normal file
4
kafka-manager-console/src/component/chart/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './bar-chart';
|
||||
export * from './date-picker-chart';
|
||||
export * from './doughnut-chart';
|
||||
export * from './line-chart';
|
||||
55
kafka-manager-console/src/component/chart/line-chart.tsx
Normal file
55
kafka-manager-console/src/component/chart/line-chart.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import echarts, { EChartOption } from 'echarts/lib/echarts';
|
||||
import 'echarts/lib/chart/pie';
|
||||
import 'echarts/lib/chart/line';
|
||||
import 'echarts/lib/component/legend';
|
||||
import 'echarts/lib/component/tooltip';
|
||||
import 'echarts/lib/component/title';
|
||||
import 'echarts/lib/component/axis';
|
||||
import './index.less';
|
||||
|
||||
export interface IEchartsProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
options?: EChartOption;
|
||||
}
|
||||
|
||||
export const hasData = (options: EChartOption) => {
|
||||
if (options && options.series && options.series.length) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
export default class LineChart extends React.Component<IEchartsProps> {
|
||||
public id = null as HTMLDivElement;
|
||||
|
||||
public myChart = null as echarts.ECharts;
|
||||
|
||||
public componentDidMount() {
|
||||
const { options } = this.props;
|
||||
this.myChart = echarts.init(this.id);
|
||||
this.myChart.setOption(options);
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public refresh = () => {
|
||||
const { options } = this.props;
|
||||
this.myChart.setOption(options);
|
||||
}
|
||||
|
||||
public resize = () => {
|
||||
this.myChart.resize();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { height, width } = this.props;
|
||||
return <div ref={id => this.id = id} style={{width: `${width}px`, height: `${height}px`}} />;
|
||||
}
|
||||
}
|
||||
55
kafka-manager-console/src/component/clipboard/index.tsx
Executable file
55
kafka-manager-console/src/component/clipboard/index.tsx
Executable file
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import ClipboardJS from 'clipboard';
|
||||
import {
|
||||
message,
|
||||
} from 'component/antd';
|
||||
|
||||
const triggerEvent = (eventName: string, element: Element) => {
|
||||
let event;
|
||||
const ele = element || document;
|
||||
|
||||
event = document.createEvent('HTMLEvents');
|
||||
event.initEvent(eventName, true, true);
|
||||
ele.dispatchEvent(event);
|
||||
};
|
||||
|
||||
export class Clipboard extends React.Component<any> {
|
||||
public state = {
|
||||
text: '',
|
||||
};
|
||||
|
||||
private clipboard: any = null;
|
||||
private dom: Element = null;
|
||||
|
||||
public componentDidMount() {
|
||||
const clipboard = this.clipboard = new ClipboardJS('.___clipboard', {
|
||||
text(trigger: Element) {
|
||||
return trigger.getAttribute('data-text');
|
||||
},
|
||||
});
|
||||
|
||||
clipboard.on('success', (e: any) => {
|
||||
message.success('复制成功!');
|
||||
e.clearSelection();
|
||||
});
|
||||
|
||||
clipboard.on('error', (e: any) => {
|
||||
message.error('复制失败!' + e);
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
|
||||
public copy(text: string) {
|
||||
this.setState({ text });
|
||||
setTimeout(() => triggerEvent('click', this.dom), 0);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="___clipboard" data-text={this.state.text} ref={dom => this.dom = dom} />
|
||||
);
|
||||
}
|
||||
}
|
||||
39
kafka-manager-console/src/component/expand-card/index.less
Normal file
39
kafka-manager-console/src/component/expand-card/index.less
Normal file
@@ -0,0 +1,39 @@
|
||||
.card-wrapper {
|
||||
margin: 24px 0 32px;
|
||||
}
|
||||
.card-title {
|
||||
font-family: PingFangSC-Medium;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
margin: 15px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
i {
|
||||
font-size: 15px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.card-content {
|
||||
background-color: #FAFAFA;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
.chart-row {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.chart-row:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.chart-wrapper {
|
||||
background-color: #FFFFFF;
|
||||
width: calc(50% - 8px);
|
||||
float: left;
|
||||
padding: 16px;
|
||||
}
|
||||
.chart-wrapper:nth-child(2n) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
48
kafka-manager-console/src/component/expand-card/index.tsx
Normal file
48
kafka-manager-console/src/component/expand-card/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import './index.less';
|
||||
import { Icon } from 'component/antd';
|
||||
|
||||
interface ICardProps {
|
||||
title: string;
|
||||
expand?: boolean;
|
||||
charts?: JSX.Element[];
|
||||
}
|
||||
|
||||
export class ExpandCard extends React.Component<ICardProps> {
|
||||
public state = {
|
||||
innerExpand: true,
|
||||
};
|
||||
|
||||
public handleClick = () => {
|
||||
this.setState({ innerExpand: !this.state.innerExpand });
|
||||
}
|
||||
|
||||
public render() {
|
||||
let { expand } = this.props;
|
||||
if (expand === undefined) expand = this.state.innerExpand;
|
||||
const { charts } = this.props;
|
||||
return (
|
||||
<div className="card-wrapper">
|
||||
{/* <div className="card-title" onClick={this.handleClick}>
|
||||
<Icon
|
||||
type={expand ? 'down' : 'up'}
|
||||
className={expand ? 'dsui-icon-jiantouxiangxia' : 'dsui-icon-jiantouxiangshang'}
|
||||
/>
|
||||
{this.props.title}
|
||||
</div> */}
|
||||
{expand ?
|
||||
<div className="card-content">
|
||||
{(charts || []).map((c, index) => {
|
||||
if (index % 2 !== 0) return null;
|
||||
return (
|
||||
<div className="chart-row" key={index}>
|
||||
<div className="chart-wrapper">{c}</div>
|
||||
{(index + 1 < charts.length) ? <div className="chart-wrapper">{charts[index + 1]}</div> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
91
kafka-manager-console/src/component/flow-table/index.tsx
Normal file
91
kafka-manager-console/src/component/flow-table/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { Table } from 'component/antd';
|
||||
|
||||
interface IFlow {
|
||||
key: string;
|
||||
avr: number;
|
||||
pre1: number;
|
||||
pre5: number;
|
||||
pre15: number;
|
||||
}
|
||||
|
||||
const flowColumns = [{
|
||||
title: '名称',
|
||||
dataIndex: 'key',
|
||||
key: 'name',
|
||||
sorter: (a: IFlow, b: IFlow) => a.key.charCodeAt(0) - b.key.charCodeAt(0),
|
||||
render(t: string) {
|
||||
return t === 'byteRejected' ? 'byteRejected(B/s)' : (t === 'byteIn' || t === 'byteOut' ? `${t}(KB/s)` : t);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '平均数',
|
||||
dataIndex: 'avr',
|
||||
key: 'partition_num',
|
||||
sorter: (a: IFlow, b: IFlow) => b.avr - a.avr,
|
||||
},
|
||||
{
|
||||
title: '前1分钟',
|
||||
dataIndex: 'pre1',
|
||||
key: 'byte_input',
|
||||
sorter: (a: IFlow, b: IFlow) => b.pre1 - a.pre1,
|
||||
},
|
||||
{
|
||||
title: '前5分钟',
|
||||
dataIndex: 'pre5',
|
||||
key: 'byte_output',
|
||||
sorter: (a: IFlow, b: IFlow) => b.pre5 - a.pre5,
|
||||
},
|
||||
{
|
||||
title: '前15分钟',
|
||||
dataIndex: 'pre15',
|
||||
key: 'message',
|
||||
sorter: (a: IFlow, b: IFlow) => b.pre15 - a.pre15,
|
||||
}];
|
||||
|
||||
export interface IFlowInfo {
|
||||
byteIn: number[];
|
||||
byteOut: number[];
|
||||
byteRejected: number[];
|
||||
failedFetchRequest: number[];
|
||||
failedProduceRequest: number[];
|
||||
messageIn: number[];
|
||||
totalFetchRequest: number[];
|
||||
totalProduceRequest: number[];
|
||||
[key: string]: number[];
|
||||
}
|
||||
|
||||
export class StatusGraghCom<T extends IFlowInfo> extends React.Component {
|
||||
public getData(): T {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getLoading(): boolean {
|
||||
return null;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const statusData = this.getData();
|
||||
const loading = this.getLoading();
|
||||
if (!statusData) return null;
|
||||
const data: any[] = [];
|
||||
Object.keys(statusData).map((key) => {
|
||||
if (statusData[key]) {
|
||||
const v = key === 'byteIn' || key === 'byteOut' ? statusData[key].map(i => i && (i / 1024).toFixed(2)) :
|
||||
statusData[key].map(i => i && i.toFixed(2));
|
||||
const obj = {
|
||||
key,
|
||||
avr: v[0],
|
||||
pre1: v[1],
|
||||
pre5: v[2],
|
||||
pre15: v[3],
|
||||
};
|
||||
data.push(obj);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Table columns={flowColumns} dataSource={data} pagination={false} loading={loading}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
136
kafka-manager-console/src/component/virtual-scroll-select.tsx
Normal file
136
kafka-manager-console/src/component/virtual-scroll-select.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as React from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { Select, Tooltip } from 'component/antd';
|
||||
import { ILabelValue } from 'types/base-type';
|
||||
import { searchProps } from 'constants/table';
|
||||
|
||||
interface IAttars {
|
||||
mode?: 'multiple' | 'tags' | 'default' | 'combobox';
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface ISelectProps {
|
||||
onChange: (result: string[] | string) => any;
|
||||
value?: string[] | string;
|
||||
isDisabled?: boolean;
|
||||
attrs?: IAttars;
|
||||
getData: () => any;
|
||||
refetchData?: boolean; // 有些页面通过store拿数据需要二次更新
|
||||
}
|
||||
export class VirtualScrollSelect extends React.Component<ISelectProps> {
|
||||
public static getDerivedStateFromProps(nextProps: any, prevState: any) {
|
||||
if (nextProps.refetchData) {
|
||||
return {
|
||||
...prevState,
|
||||
refetchData: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public state = {
|
||||
optionsData: [] as ILabelValue[],
|
||||
scrollPage: 0,
|
||||
keyword: '',
|
||||
total: 0,
|
||||
refetchData: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.getData();
|
||||
}
|
||||
|
||||
public getData = async () => {
|
||||
const { getData } = this.props;
|
||||
if (!getData) return;
|
||||
const pageSize = this.state.scrollPage;
|
||||
let originData = await getData();
|
||||
|
||||
if (originData) {
|
||||
originData = this.state.keyword ?
|
||||
originData.filter((item: any) => item.label.includes(this.state.keyword)) : originData;
|
||||
let data = [].concat(originData);
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
const total = data.length ? data.length / 30 | 1 : 0;
|
||||
data = data.splice(pageSize * 30, 30); // 每页展示30条数据
|
||||
|
||||
return this.setState({
|
||||
optionsData: data,
|
||||
total,
|
||||
refetchData: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: any) {
|
||||
if (this.state.refetchData && !this.state.optionsData.length) {
|
||||
// this.getData();
|
||||
}
|
||||
}
|
||||
|
||||
public handleSearch = (e: string) => {
|
||||
debounce(() => {
|
||||
this.setState({
|
||||
keyword: e.trim(),
|
||||
scrollPage: 0,
|
||||
}, () => {
|
||||
this.getData();
|
||||
});
|
||||
}, 300)();
|
||||
}
|
||||
|
||||
public handleSelectScroll = (e: any) => {
|
||||
e.persist();
|
||||
const { target } = e;
|
||||
const { scrollPage } = this.state;
|
||||
debounce(() => {
|
||||
if (target.scrollTop + target.offsetHeight === target.scrollHeight) {
|
||||
const nextScrollPage = scrollPage + 1;
|
||||
if (this.state.total <= nextScrollPage) { // 已全部拉取
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
scrollPage: nextScrollPage,
|
||||
}, () => {
|
||||
this.getData();
|
||||
});
|
||||
}
|
||||
if (target.scrollTop === 0 && scrollPage !== 0) { // 往上滚且不是第一页
|
||||
const nextScrollPage = scrollPage - 1;
|
||||
this.setState({
|
||||
scrollPage: nextScrollPage,
|
||||
}, () => {
|
||||
this.getData();
|
||||
});
|
||||
}
|
||||
}, 200)();
|
||||
}
|
||||
|
||||
public render() {
|
||||
// tslint:disable-next-line:prefer-const
|
||||
let { value, isDisabled, attrs } = this.props;
|
||||
if (attrs && (attrs.mode === 'multiple' || attrs.mode === 'tags')) {
|
||||
value = value || [];
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
{...attrs}
|
||||
defaultValue={value}
|
||||
value={value}
|
||||
onChange={this.props.onChange}
|
||||
onSearch={this.handleSearch}
|
||||
disabled={isDisabled}
|
||||
onPopupScroll={this.handleSelectScroll}
|
||||
{...searchProps}
|
||||
>
|
||||
{this.state.optionsData.map((d: ILabelValue) =>
|
||||
<Select.Option value={d.value} key={d.value}>
|
||||
{d.label.length > 25 ? <Tooltip placement="bottomLeft" title={d.label}>
|
||||
{d.label.substring(0, 25) + '...'}
|
||||
</Tooltip> : d.label}
|
||||
</Select.Option>)}
|
||||
</Select>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
156
kafka-manager-console/src/component/x-form-wrapper/index.tsx
Executable file
156
kafka-manager-console/src/component/x-form-wrapper/index.tsx
Executable file
@@ -0,0 +1,156 @@
|
||||
import * as React from 'react';
|
||||
import { Drawer, Modal, Button, message } from 'component/antd';
|
||||
import { XFormComponent } from 'component/x-form';
|
||||
import { IXFormWrapper } from 'types/base-type';
|
||||
|
||||
export class XFormWrapper extends React.Component<IXFormWrapper> {
|
||||
|
||||
public state = {
|
||||
confirmLoading: false,
|
||||
formMap: this.props.formMap || [] as any,
|
||||
formData: this.props.formData || {},
|
||||
};
|
||||
|
||||
private $formRef: any;
|
||||
|
||||
public updateFormMap$(formMap?: any, formData?: any, isResetForm?: boolean, resetFields?: string[]) {
|
||||
if (isResetForm) {
|
||||
resetFields ? this.resetForm(resetFields) : this.resetForm();
|
||||
}
|
||||
this.setState({
|
||||
formMap,
|
||||
formData,
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { type } = this.props;
|
||||
switch (type) {
|
||||
case 'drawer':
|
||||
return this.renderDrawer();
|
||||
default:
|
||||
return this.renderModal();
|
||||
}
|
||||
}
|
||||
|
||||
public renderDrawer() {
|
||||
const {
|
||||
visible,
|
||||
title,
|
||||
width,
|
||||
formData,
|
||||
formMap,
|
||||
formLayout,
|
||||
cancelText,
|
||||
okText,
|
||||
customRenderElement,
|
||||
noform,
|
||||
nofooter,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={title}
|
||||
visible={visible}
|
||||
width={width}
|
||||
closable={true}
|
||||
onClose={this.handleCancel}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<>
|
||||
{customRenderElement}
|
||||
</>
|
||||
{!noform && (
|
||||
<XFormComponent
|
||||
ref={form => this.$formRef = form}
|
||||
formData={formData}
|
||||
formMap={formMap}
|
||||
formLayout={formLayout}
|
||||
/>)}
|
||||
{!nofooter && (<div className="footer-btn">
|
||||
<Button type="primary" onClick={this.handleSubmit}>{okText || '确认'}</Button>
|
||||
<Button onClick={this.handleCancel}>{cancelText || '取消'}</Button>
|
||||
</div>)}
|
||||
<>
|
||||
</>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
public renderModal() {
|
||||
const { visible, title, width, formLayout, cancelText, okText, customRenderElement } = this.props;
|
||||
const { formMap, formData } = this.state;
|
||||
return (
|
||||
<Modal
|
||||
width={width}
|
||||
title={title}
|
||||
visible={visible}
|
||||
confirmLoading={this.state.confirmLoading}
|
||||
maskClosable={false}
|
||||
onOk={this.handleSubmit}
|
||||
onCancel={this.handleCancel}
|
||||
okText={okText || '确认'}
|
||||
cancelText={cancelText || '取消'}
|
||||
>
|
||||
<XFormComponent
|
||||
ref={form => this.$formRef = form}
|
||||
formData={formData}
|
||||
formMap={formMap}
|
||||
formLayout={formLayout}
|
||||
/>
|
||||
<>{customRenderElement}</>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
public handleSubmit = () => {
|
||||
this.$formRef.validateFields((error: Error, result: any) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
const { onSubmit, isWaitting } = this.props;
|
||||
|
||||
if (typeof onSubmit === 'function') {
|
||||
if (isWaitting) {
|
||||
this.setState({
|
||||
confirmLoading: true,
|
||||
});
|
||||
onSubmit(result).then(() => {
|
||||
this.setState({
|
||||
confirmLoading: false,
|
||||
});
|
||||
message.success('操作成功');
|
||||
this.resetForm();
|
||||
this.closeModalWrapper();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onSubmit && onSubmit(result);
|
||||
|
||||
this.resetForm();
|
||||
this.closeModalWrapper();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public handleCancel = () => {
|
||||
const { onCancel } = this.props;
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onCancel && onCancel();
|
||||
this.resetForm();
|
||||
this.closeModalWrapper();
|
||||
}
|
||||
|
||||
public resetForm = (resetFields?: string[]) => {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
this.$formRef && this.$formRef.resetFields(resetFields || '');
|
||||
}
|
||||
|
||||
public closeModalWrapper = () => {
|
||||
const { onChangeVisible } = this.props;
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
onChangeVisible && onChangeVisible(false);
|
||||
}
|
||||
}
|
||||
11
kafka-manager-console/src/component/x-form/index.less
Normal file
11
kafka-manager-console/src/component/x-form/index.less
Normal file
@@ -0,0 +1,11 @@
|
||||
.ant-input-number {
|
||||
width: 314px
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
float: right;
|
||||
|
||||
Button:first-child {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
197
kafka-manager-console/src/component/x-form/index.tsx
Executable file
197
kafka-manager-console/src/component/x-form/index.tsx
Executable file
@@ -0,0 +1,197 @@
|
||||
import * as React from 'react';
|
||||
import { Select, Input, InputNumber, Form, Switch, Checkbox, DatePicker, Radio, Upload, Button, Icon, Tooltip } from 'component/antd';
|
||||
import { searchProps } from 'constants/table';
|
||||
import './index.less';
|
||||
|
||||
const TextArea = Input.TextArea;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
export enum FormItemType {
|
||||
input = 'input',
|
||||
inputPassword = 'input_password',
|
||||
inputNumber = 'input_number',
|
||||
textArea = 'text_area',
|
||||
select = 'select',
|
||||
_switch = '_switch',
|
||||
custom = 'custom',
|
||||
checkBox = 'check_box',
|
||||
datePicker = 'date_picker',
|
||||
rangePicker = 'range_picker',
|
||||
radioGroup = 'radio_group',
|
||||
upload = 'upload',
|
||||
}
|
||||
|
||||
export interface IFormItem {
|
||||
key: string;
|
||||
label: string;
|
||||
type: FormItemType;
|
||||
value?: string;
|
||||
// 内部组件属性注入
|
||||
attrs?: any;
|
||||
// form属性注入
|
||||
formAttrs?: any;
|
||||
defaultValue?: string | number | any[];
|
||||
rules?: any[];
|
||||
invisible?: boolean;
|
||||
renderExtraElement?: () => JSX.Element;
|
||||
}
|
||||
|
||||
export interface IFormSelect extends IFormItem {
|
||||
options: Array<{ key?: string | number, value: string | number, label: string, text?: string }>;
|
||||
}
|
||||
|
||||
interface IFormCustom extends IFormItem {
|
||||
customFormItem: React.Component;
|
||||
}
|
||||
|
||||
interface IXFormProps {
|
||||
formMap: IFormItem[];
|
||||
formData: any;
|
||||
form: any;
|
||||
formLayout?: any;
|
||||
layout?: 'inline' | 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
class XForm extends React.Component<IXFormProps> {
|
||||
|
||||
private defaultFormLayout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 16 },
|
||||
};
|
||||
|
||||
public onUploadFileChange = (e: any) => {
|
||||
if (Array.isArray(e)) {
|
||||
return e;
|
||||
}
|
||||
return e && e.fileList;
|
||||
}
|
||||
|
||||
public handleFormItem(formItem: any, formData: any) {
|
||||
let initialValue = formData[formItem.key] === 0 ? 0 : (formData[formItem.key] || formItem.defaultValue || '');
|
||||
let valuePropName = 'value';
|
||||
|
||||
if (formItem.type === FormItemType.datePicker) {
|
||||
initialValue = initialValue || null;
|
||||
}
|
||||
|
||||
// if (formItem.type === FormItemType.checkBox) {
|
||||
// initialValue = formItem.defaultValue ? [formItem.defaultValue] : [];
|
||||
// }
|
||||
|
||||
if (formItem.type === FormItemType._switch) {
|
||||
initialValue = false;
|
||||
}
|
||||
|
||||
// if (formItem.type === FormItemType.select && formItem.attrs
|
||||
// && ['tags'].includes(formItem.attrs.mode)) {
|
||||
// initialValue = formItem.defaultValue ? [formItem.defaultValue] : [];
|
||||
// }
|
||||
|
||||
if (formItem.type === FormItemType._switch) {
|
||||
valuePropName = 'checked';
|
||||
}
|
||||
|
||||
if (formItem.type === FormItemType.upload) {
|
||||
valuePropName = 'fileList';
|
||||
}
|
||||
|
||||
return { initialValue, valuePropName };
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { form, formData, formMap, formLayout, layout } = this.props;
|
||||
const { getFieldDecorator } = form;
|
||||
return (
|
||||
<Form layout={layout || 'horizontal'} onSubmit={() => ({})}>
|
||||
{formMap.map(formItem => {
|
||||
const { initialValue, valuePropName } = this.handleFormItem(formItem, formData);
|
||||
|
||||
const getFieldValue = {
|
||||
initialValue,
|
||||
rules: formItem.rules || [{ required: false, message: '' }],
|
||||
valuePropName,
|
||||
};
|
||||
|
||||
if (formItem.type === FormItemType.upload) {
|
||||
Object.assign(getFieldValue, {
|
||||
getValueFromEvent: this.onUploadFileChange,
|
||||
});
|
||||
}
|
||||
return (
|
||||
!formItem.invisible &&
|
||||
<Form.Item
|
||||
key={formItem.key}
|
||||
label={formItem.label}
|
||||
{...(formLayout || (layout === 'inline' ? {} : this.defaultFormLayout))}
|
||||
{...formItem.formAttrs}
|
||||
>
|
||||
{getFieldDecorator(formItem.key, getFieldValue)(
|
||||
this.renderFormItem(formItem),
|
||||
)}
|
||||
{formItem.renderExtraElement ? formItem.renderExtraElement() : null}
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
public renderFormItem(item: IFormItem) {
|
||||
|
||||
switch (item.type) {
|
||||
default:
|
||||
case FormItemType.input:
|
||||
return <Input key={item.key} {...item.attrs} />;
|
||||
case FormItemType.inputPassword:
|
||||
return <Input.Password key={item.key} {...item.attrs} />;
|
||||
case FormItemType.inputNumber:
|
||||
return <InputNumber {...item.attrs} />;
|
||||
case FormItemType.textArea:
|
||||
return <TextArea rows={5} {...item.attrs} />;
|
||||
case FormItemType.select:
|
||||
return (
|
||||
<Select
|
||||
key={item.key}
|
||||
{...item.attrs}
|
||||
{...searchProps}
|
||||
>
|
||||
{(item as IFormSelect).options && (item as IFormSelect).options.map((v, index) => (
|
||||
<Select.Option
|
||||
key={v.value || v.key || index}
|
||||
value={v.value}
|
||||
>
|
||||
{v.label.length > 35 ? <Tooltip placement="bottomLeft" title={v.text || v.label}>
|
||||
{v.label}
|
||||
</Tooltip> : v.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
case FormItemType._switch:
|
||||
return <Switch {...item.attrs} />;
|
||||
case FormItemType.custom:
|
||||
return (item as IFormCustom).customFormItem;
|
||||
case FormItemType.checkBox:
|
||||
return <Checkbox.Group options={(item as IFormSelect).options} />;
|
||||
case FormItemType.datePicker:
|
||||
return <DatePicker key={item.key} {...item.attrs} />;
|
||||
case FormItemType.rangePicker:
|
||||
return <RangePicker key={item.key} {...item.attrs} />;
|
||||
case FormItemType.radioGroup:
|
||||
return (
|
||||
<Radio.Group key={item.key} {...item.attrs}>
|
||||
{(item as IFormSelect).options.map((v, index) => (
|
||||
<Radio.Button key={v.value || v.key || index} value={v.value}>{v.label}</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>);
|
||||
case FormItemType.upload:
|
||||
return (
|
||||
<Upload beforeUpload={(file: any) => false} {...item.attrs}>
|
||||
<Button><Icon type="upload" />上传</Button>
|
||||
</Upload>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const XFormComponent = Form.create<IXFormProps>()(XForm);
|
||||
Reference in New Issue
Block a user