kafka-manager 2.0

This commit is contained in:
zengqiao
2020-09-28 15:46:34 +08:00
parent 28d985aaf1
commit c6e4b60424
1253 changed files with 82183 additions and 37179 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,5 @@
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View 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,
};

View 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>
</>
);
}
}

View 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>
);
}
}

View 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>
</>
);
}
}

View 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;
}

View File

@@ -0,0 +1,4 @@
export * from './bar-chart';
export * from './date-picker-chart';
export * from './doughnut-chart';
export * from './line-chart';

View 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`}} />;
}
}

View 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} />
);
}
}

View 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;
}
}

View 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>
);
}
}

View 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}/>
);
}
}

View 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>
</>
);
}
}

View 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);
}
}

View File

@@ -0,0 +1,11 @@
.ant-input-number {
width: 314px
}
.footer-btn {
float: right;
Button:first-child {
margin-right: 16px;
}
}

View 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);

View File

@@ -0,0 +1,80 @@
export const urlPrefix = '/kafka';
import { ILeftMenu } from 'types/base-type';
export const topicMenu = [{
href: `/topic`,
i: 'k-icon-iconfontzhizuobiaozhun023110',
title: '我的Topic',
}, {
href: `/topic/topic-all`,
i: 'k-icon-order1',
title: '全部Topic',
}, {
href: `/topic/app-list`,
i: 'k-icon-gaojing',
title: '应用管理',
}] as ILeftMenu[];
export const clusterMenu = [{
href: `/cluster`,
i: 'k-icon-jiqun',
title: '我的集群',
}] as ILeftMenu[];
export const alarmMenu = [{
href: `/alarm`,
i: 'k-icon-jiqun',
title: '监控告警',
}] as ILeftMenu[];
export const userMenu = [{
href: `/user/my-order`,
i: 'k-icon-order1',
title: '我的申请',
class: 'apply',
}, {
href: `/user/my-approval`,
i: 'k-icon-shenpi1',
title: '我的审批',
class: 'approval',
}, {
href: `/user/bill`,
i: 'k-icon-gaojing',
title: '账单管理',
}] as ILeftMenu[];
export const adminMenu = [{
href: `/admin`,
i: 'k-icon-jiqun',
title: '集群列表',
}, {
href: `/admin/operation`,
i: 'k-icon-xiaofeikecheng',
title: '集群运维',
}, {
href: `/admin/app`,
i: 'k-icon-order1',
title: '平台管理',
}, {
href: `/admin/bill`,
i: 'k-icon-renwuliebiao',
title: '用户账单',
}] as ILeftMenu[];
export const expertMenu = [{
href: `/expert`,
i: 'k-icon-jiqun',
title: 'Topic分区热点',
}, {
href: `/expert/topic-partition`,
i: 'k-icon-order1',
title: 'Topic分区不足',
}, {
href: `/expert/topic-governance`,
i: 'k-icon-order1',
title: 'Topic资源治理',
}, {
href: `/expert/diagnosis`,
i: 'k-icon-xiaofeikecheng',
title: '异常诊断',
}] as ILeftMenu[];

View File

@@ -0,0 +1,209 @@
import { IStatusMap, IStringMap, ILabelValue } from 'types/base-type';
export const optionMap = [
'byteIn/byteOut',
'bytesRejectedPerSec',
'failFetchRequestPerSec',
'failProduceRequestPerSec',
'fetchConsumerRequestPerSec',
'healthScore',
'logFlushTime',
'messagesInPerSec',
'networkProcessorIdlPercent',
'produceRequestPerSec',
'requestHandlerIdlPercent',
'requestQueueSize',
'responseQueueSize',
'totalTimeFetchConsumer99Th',
'totalTimeProduce99Th',
];
export const copyValueMap = ['同步', '未同步'];
export const appStatusMap = {
0: '待审批',
1: '已通过',
2: '被拒绝',
} as IStatusMap;
export const topicStatusMap = {
0: '无权限',
1: '可消费',
2: '可发送',
3: '可发送、消费',
4: '可管理',
} as IStatusMap;
export const authStatusMap = {
0: '无权限',
1: '消费',
2: '发送',
3: '发送、消费',
4: '管理',
} as IStatusMap;
export const orderStatusMap = {
0: '待审批',
1: '已通过',
2: '已拒绝',
3: '已取消',
} as IStatusMap;
export const clusterTypeMap = {
0: '共享集群',
1: '独享集群',
2: '独立集群',
} as IStatusMap;
export const classStatusMap = {
'-1': 'executing',
'0': 'pending',
'10': 'executing',
'20': 'pending',
'30': 'executing',
'40': 'success',
'41': 'success',
'42': 'fail',
'43': 'cancel',
'44': 'executing',
'45': 'pending',
} as IStatusMap;
export const orderApiTypeMap = {
0: 'topics',
1: 'apps',
2: 'quotas',
3: 'authorities',
4: 'clusters',
} as IStatusMap;
export const offlineStatusMap = {
'-1': '可下线',
'0': '过期待通知',
'1': '已通知待反馈',
} as IStatusMap;
export const orderApiMap = {
0: '/normal/orders/topics',
1: '/normal/orders/apps',
2: '/normal/orders/quotas',
3: '/normal/orders/authorities',
4: '/normal/orders/clusters',
} as IStatusMap;
export const controlOptionMap = [{
label: 'Bytes In/Bytes Out',
value: 'byteIn/byteOut',
}, {
label: 'Message In',
value: 'messageIn',
}, {
label: 'Topic Num',
value: 'topicNum',
}, {
label: 'Broker Num',
value: 'brokerNum',
}] as ILabelValue[];
export const selectOptionMap = [{
label: 'Bytes In/Bytes Out',
value: 'byteIn/byteOut',
}, {
label: 'Bytes Rejected',
value: 'byteRejected',
}, {
label: 'Message In/TotalProduceRequests',
value: 'messageIn/totalProduceRequests',
}] as ILabelValue[];
export const selectBrokerMap = [{
label: 'Bytes In/Bytes Out',
value: 'byteIn/byteOut',
}, {
label: 'Bytes Rejected',
value: 'byteRejected',
}, {
label: 'Message In',
value: 'messageIn',
}] as ILabelValue[];
export const metricOptionMap = [
{
type: 'byteIn/byteOut',
arr: ['bytesInPerSec', 'bytesOutPerSec'],
},
{
type: 'messageIn/totalProduceRequests',
arr: ['messagesInPerSec', 'totalProduceRequestsPerSec'],
},
{
type: 'byteRejected',
arr: ['bytesRejectedPerSec'],
},
{
type: 'byteIn/byteOut/appByteIn/appByteOut',
// tslint:disable-next-line:max-line-length
// arr: ['bytesInPerSec', 'bytesOutPerSec', 'appIdBytesInPerSec', 'appIdBytesOutPerSec', 'consumeThrottled', 'produceThrottled'],
arr: ['bytesInPerSec', 'bytesOutPerSec'],
},
];
export const selectMonitorMap = [{
label: '美国',
value: '美国',
}, {
label: '中国',
value: '中国',
}, {
label: '俄罗斯',
value: '俄罗斯',
}] as ILabelValue[];
export const selectTakeMap = [{
label: 'RequestTime99thPercentile',
value: 'requestTime99thPercentile',
}, {
label: 'RequestTime95thPercentile',
value: 'requestTime95thPercentile',
}, {
label: 'RequestTime75thPercentile',
value: 'requestTime75thPercentile',
}, {
label: 'RequestTime50thPercentile',
value: 'requestTime50thPercentile',
}] as ILabelValue[];
export const columsDefault = {
leaderPartitionList: 'leaderPartitions:',
followerPartitionIdList: 'followerPartitions:',
notUnderReplicatedPartitionIdList: 'notUnderReplicatedPartitions:',
} as IStringMap;
export const diskDefault = {
leaderPartitions: 'leaderPartitions:',
followerPartitions: 'followerPartitions:',
notUnderReplicatedPartitions: 'notUnderReplicatedPartitions:',
} as IStringMap;
export const brokerMetrics = {
bytesIn: 'Bytes InMB/ 秒)',
bytesOut: 'Bytes OutMB/ 秒)',
messagesIn: 'Messages In条)',
totalFetchRequests: 'Total Fetch RequestsQPS)',
totalProduceRequests: 'Total Produce RequestsQPS)',
} as IStringMap;
export const roleMap = {
0: '普通用户',
1: '研发人员',
2: '运维人员',
} as IStatusMap;
export const weekOptions = [
{ label: '周一', value: 1 },
{ label: '周二', value: 2 },
{ label: '周三', value: 3 },
{ label: '周四', value: 4 },
{ label: '周五', value: 5 },
{ label: '周六', value: 6 },
{ label: '周日', value: 0 },
];

View File

@@ -0,0 +1,74 @@
export const timeOptions = [
{ label: '周日', value: '0' },
{ label: '周一', value: '1' },
{ label: '周二', value: '2' },
{ label: '周三', value: '3' },
{ label: '周四', value: '4' },
{ label: '周五', value: '5' },
{ label: '周六', value: '6' },
];
export const equalList = [
{ label: '大于', value: '>' },
{ label: '小于', value: '<' },
{ label: '等于', value: '=' },
{ label: '大于等于', value: '>=' },
{ label: '小于等于', value: '<=' },
{ label: '不等于', value: '!=' },
];
export const timeList = [
{ label: '天', value: 'day' },
{ label: '小时', value: 'hour' },
];
export const funcList = [
{ label: '周期发生-happen', value: 'happen' },
{ label: '连续发生-all', value: 'all' },
{ label: '同比变化率-c_avg_rate_abs', value: 'c_avg_rate_abs' },
{ label: '突增突降值-diff', value: 'diff' },
{ label: '突增突降率-pdiff', value: 'pdiff' },
{ label: '求和-sum', value: 'sum' },
];
interface IStringArray {
[key: string]: string [];
}
export const funcKeyMap = {
happen: ['period', 'count'],
ndiff: ['period', 'count'],
c_avg_rate_abs: ['period', 'day'],
all: ['period'],
diff: ['period'],
pdiff: ['period'],
sum: ['period'],
} as IStringArray;
export const filterList = [
{ label: '集群', value: 'clusterName' },
{ label: 'Topic', value: 'topic' },
{ label: 'Location', value: 'loaction' },
{ label: '消费组', value: 'consumerGroup' },
];
export const filterKeys = ['clusterName', 'topic', 'location', 'consumerGroup'];
export const timeFormat = 'YYYY-MM-DD HH:mm:ss';
export const timeMinute = 'YYYY-MM-DD HH:mm';
export const timeMonth = 'YYYY-MM';
export const timeStampStr = 'YYYY/MM/DD HH:mm:ss';
export const timeMonthStr = 'YYYY/MM';
// tslint:disable-next-line:max-line-length
export const indexUrl = 'https://github.com/didi/kafka-manager';
export const expandRemarks = `请填写不少于5字的申请原因以便工作人员判断审核`;
export const quotaRemarks = `请填写不少于5字的申请原因以便工作人员判断审核\n\n如需申请分区分区标准为3MB/s一个请填写“分区数n”`;

View File

@@ -0,0 +1,35 @@
import { PaginationConfig } from 'component/antd';
export const pagination: PaginationConfig = {
position: 'bottom',
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
showTotal: (total) => `${total}`,
};
export const customPagination: PaginationConfig = {
position: 'bottom',
showQuickJumper: true,
showSizeChanger: true,
showTotal: (total) => `${total}`,
};
export const cellStyle = {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
};
export const searchProps = {
optionFilterProp: 'children',
showSearch: true,
filterOption: (input: any, option: any) => {
if ( typeof option.props.children === 'object' ) {
const { props } = option.props.children as any;
return (props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
}
return (option.props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
};

View File

@@ -0,0 +1,47 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import 'styles/table-filter.less';
import { app } from 'store/app';
import { CommonAppList } from 'container/app/app-list';
@observer
export class AdminAppList extends CommonAppList {
public static defaultProps = {
from: 'admin',
};
constructor(defaultProps: any) {
super(defaultProps);
}
public componentDidMount() {
if (!app.adminAppData.length) {
app.getAdminAppList();
}
}
public renderTable() {
return this.renderTableList(this.getData(app.adminAppData));
}
public renderOperationPanel() {
return (
<ul>
{this.renderSearch('', '请输入应用名称或者负责人')}
</ul>
);
}
public render() {
return (
<div className="container">
<div className="table-operation-panel">
{this.renderOperationPanel()}
</div>
<div className="table-wrapper">
{this.renderTable()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,103 @@
import * as React from 'react';
import { Table, Tabs, Icon, Spin } from 'component/antd';
import { pagination } from 'constants/table';
import { observer } from 'mobx-react';
import { bill } from 'store/bill';
import { SearchAndFilterContainer } from 'container/search-filter';
import { billDetailCols } from '../user-center/config';
import { admin } from 'store/admin';
import { IBillDetail } from 'types/base-type';
import { getCookie } from 'lib/utils';
import { timeMonth } from 'constants/strategy';
import Url from 'lib/url-parser';
import * as XLSX from 'xlsx';
import moment from 'moment';
const { TabPane } = Tabs;
@observer
export class BillDetail extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
private timestamp: number = null;
constructor(props: any) {
super(props);
const url = Url();
this.timestamp = Number(url.search.timestamp);
}
public componentDidMount() {
admin.getBillDetailStaffList(getCookie('username'), this.timestamp);
}
public getData<T extends IBillDetail>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBillDetail) =>
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public handleDownLoad() {
const tableData = admin.billDetailStaffData.map(item => {
return {
// tslint:disable
'集群ID': item.clusterId,
'集群名称': item.clusterName,
'quota数量': item.quota,
'Topic名称': item.topicName,
'金额': item.cost,
};
});
const data = [].concat(tableData);
const wb = XLSX.utils.book_new();
// json转sheet
const ws = XLSX.utils.json_to_sheet(data, {
header: ['集群ID', '集群名称', 'quota数量', 'Topic名称', '金额'],
});
// XLSX.utils.
XLSX.utils.book_append_sheet(wb, ws, 'bill');
// 输出
XLSX.writeFile(wb, 'bill-' + moment(this.timestamp).format(timeMonth) + '.xlsx');
}
public renderTableList() {
return (
<Spin spinning={bill.loading}>
<Table
rowKey="key"
columns={billDetailCols}
dataSource={this.getData(admin.billDetailStaffData)}
pagination={pagination}
/>
</Spin>
);
}
public render() {
return (
<>
<div className="container">
<Tabs defaultActiveKey="1" type="card">
<TabPane tab={`账单详情-${moment(this.timestamp).format(timeMonth)}`} key="1">
{this.renderTableList()}
</TabPane>
</Tabs>
<div className="operation-panel special">
<ul>
{this.renderSearch('', '请输入TopicName')}
<li className="right-btn-1">
<Icon type="download" onClick={this.handleDownLoad.bind(this, null)} />
</li>
</ul>
</div>
</div>
</>
);
}
}

View File

@@ -0,0 +1,5 @@
.bill-head{
width: 100%;
display: flex;
justify-content: space-between;
}

View File

@@ -0,0 +1,20 @@
import * as React from 'react';
import { handleTabKey } from 'lib/utils';
import { PersonalBill } from './personal-bill';
import { Tabs } from 'antd';
const { TabPane } = Tabs;
export class BillManagement extends React.Component {
public render() {
return(
<>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="个人账单" key="1">
<PersonalBill />
</TabPane>
</Tabs>
</>
);
}
}

View File

@@ -0,0 +1,128 @@
import * as React from 'react';
import { Table, DatePicker } from 'antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { admin } from 'store/admin';
import { Moment } from 'moment';
import { observer } from 'mobx-react';
import { IStaffSummary } from 'types/base-type';
import { pagination } from 'constants/table';
import './index.less';
const { MonthPicker } = DatePicker;
import moment = require('moment');
import { timeFormat } from 'constants/strategy';
@observer
export class PersonalBill extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
public handleTimeChange = (value: Moment) => {
const timestamp = value.valueOf();
admin.getStaffSummary(timestamp);
}
public selectTime() {
return (
<>
<div className="zoning-otspots">
<div>
<span></span>
<MonthPicker
placeholder="Select month"
defaultValue={moment()}
onChange={this.handleTimeChange}
/>
</div>
</div>
</>
);
}
public getData<T extends IStaffSummary>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IStaffSummary) =>
(item.username !== undefined && item.username !== null) && item.username.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public pendingTopic() {
const columns = [
{
title: '月份',
dataIndex: 'gmtMonth',
width: '15%',
sorter: (a: IStaffSummary, b: IStaffSummary) => b.timestamp - a.timestamp,
render: (text: string, record: IStaffSummary) => (
<a href={`${this.urlPrefix}/admin/bill-individual`}> {text} </a>),
},
{
title: '用户名',
dataIndex: 'username',
width: '20%',
sorter: (a: IStaffSummary, b: IStaffSummary) => a.username.charCodeAt(0) - b.username.charCodeAt(0),
},
{
title: 'Topic数量',
dataIndex: 'topicNum',
width: '15%',
sorter: (a: IStaffSummary, b: IStaffSummary) => b.topicNum - a.topicNum,
},
{
title: '时间',
dataIndex: 'timestamp',
width: '20%',
sorter: (a: IStaffSummary, b: IStaffSummary) => b.timestamp - a.timestamp,
render: (t: number) => moment(t).format(timeFormat),
},
{
title: 'Quota(M/S)',
dataIndex: 'quota',
width: '15%',
sorter: (a: IStaffSummary, b: IStaffSummary) => b.quota - a.quota,
render: (t: number) => t === null ? '' : Number.isInteger(t) ? t : (t).toFixed(2),
},
{
title: '金额',
dataIndex: 'cost',
width: '15%',
sorter: (a: IStaffSummary, b: IStaffSummary) => b.cost - a.cost,
render: (t: number) => t === null ? '' : Number.isInteger(t) ? t : (t).toFixed(2),
},
];
return (
<>
<ul className="bill-head">
{this.renderSearch('名称:', '请输入用户名')}
{this.selectTime()}
</ul>
<Table
columns={columns}
dataSource={this.getData(admin.staffSummary)}
pagination={pagination}
rowKey="key"
/>
</>
);
}
public componentDidMount() {
const timestamp = +moment().format('x');
admin.getStaffSummary(timestamp);
}
public render() {
return(
<>
{admin.staffSummary ? this.pendingTopic() : null}
</>
);
}
}

View File

@@ -0,0 +1,121 @@
import * as React from 'react';
import { ILabelValue, IBrokersBasicInfo, IOptionType, IClusterReal } from 'types/base-type';
import { observer } from 'mobx-react';
import moment from 'moment';
import Url from 'lib/url-parser';
import { admin } from 'store/admin';
import { PageHeader, Descriptions, Spin } from 'component/antd';
import { selectBrokerMap } from 'constants/status-map';
import { StatusGraghCom } from 'component/flow-table';
import { NetWorkFlow, renderTrafficTable } from 'container/network-flow';
import { timeFormat } from 'constants/strategy';
@observer
export class BaseInfo extends React.Component {
public clusterId: number;
public brokerId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.brokerId = Number(url.search.brokerId);
}
public updateRealStatus = () => {
admin.getBrokersMetrics(this.clusterId, this.brokerId);
}
public onSelectChange(e: IOptionType) {
return admin.changeBrokerType(e);
}
public getOptionApi = () => {
return admin.getBrokersMetricsHistory(this.clusterId, this.brokerId);
}
public componentDidMount() {
admin.getBrokersBasicInfo(this.clusterId, this.brokerId);
admin.getBrokersMetrics(this.clusterId, this.brokerId);
}
public renderBrokerContent() {
let content = {} as IBrokersBasicInfo;
content = admin.brokersBasicInfo ? admin.brokersBasicInfo : content;
const brokerContent = [{
value: content.host,
label: '主机名',
}, {
value: content.port,
label: '服务端口',
}, {
value: content.jmxPort,
label: 'JMX端口',
}, {
value: content.topicNum,
label: 'Topic数',
}, {
value: content.leaderCount,
label: 'Leader分区数',
}, {
value: content.partitionCount,
label: '分区数',
}, {
value: moment(content.startTime).format(timeFormat),
label: '启动时间',
}];
return (
<>
<div className="chart-title"></div>
<PageHeader className="detail" title="">
<Descriptions size="small" column={3}>
{brokerContent.map((item: ILabelValue, index: number) => (
<Descriptions.Item key={index} label={item.label}>{item.value}</Descriptions.Item>
))}
</Descriptions>
</PageHeader>
</>
);
}
public renderHistoryTraffic() {
return (
<NetWorkFlow
key="1"
selectArr={selectBrokerMap}
type={admin.type}
selectChange={(value: IOptionType) => this.onSelectChange(value)}
getApi={() => this.getOptionApi()}
/>
);
}
public renderTrafficInfo = () => {
return (
<Spin spinning={admin.realBrokerLoading}>
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
</Spin>
);
}
public render() {
return (
<>
{this.renderBrokerContent()}
{this.renderTrafficInfo()}
{this.renderHistoryTraffic()}
</>
);
}
}
@observer
class StatusGragh extends StatusGraghCom<IClusterReal> {
public getData = () => {
return admin.brokersMetrics;
}
public getLoading = () => {
return admin.realBrokerLoading;
}
}

View File

@@ -0,0 +1,172 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Table, Tooltip } from 'component/antd';
import { diskDefault } from 'constants/status-map';
import Url from 'lib/url-parser';
import { pagination } from 'constants/table';
import { admin } from 'store/admin';
import { IPartitionsLocation } from 'types/base-type';
import { SearchAndFilterContainer } from 'container/search-filter';
import './index.less';
@observer
export class DiskInfo extends SearchAndFilterContainer {
public clusterId: number;
public brokerId: number;
public state = {
searchKey: '',
filterStatusVisible: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.brokerId = Number(url.search.brokerId);
}
public getDescription = (value: any, record: any) => {
return Object.keys(value).map((key: keyof any, index: any) => {
return (
<>
<p key={index}>
<span>{value[key]}</span>
{(record[key] as []).join(',')}{(record[key] as []).length})
</p>
</>
);
});
}
public getMoreDetail = (record: IPartitionsLocation) => {
return (
<div className="p-description" key={record.key}>
<p><span>diskName: </span>{record.diskName}</p>
<p><span>brokerId: </span>{record.brokerId}</p>
<p><span>isUnderReplicated:</span>{record.underReplicated + ''}</p>
<p><span>topic: </span>{record.topicName}</p>
{this.getDescription(diskDefault, record)}
<p><span>clusterId: </span>{record.clusterId}</p>
<p><span>underReplicatedPartitions: </span></p>
</div>
);
}
public componentDidMount() {
admin.getPartitionsLocation(this.clusterId, this.brokerId);
}
public getData<T extends IPartitionsLocation>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IPartitionsLocation) =>
(item.diskName !== undefined && item.diskName !== null) && item.diskName.toLowerCase().includes(searchKey as string)
|| (item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderDiskInfo() {
const underReplicated = Object.assign({
title: '状态',
dataIndex: 'underReplicated',
key: 'underReplicated',
filters: [{ text: '已同步', value: 'false' }, { text: '未同步', value: 'true' }],
onFilter: (value: string, record: IPartitionsLocation) => record.underReplicated + '' === value,
render: (t: boolean) => <span>{t ? '未同步' : '已同步'}</span>,
}, this.renderColumnsFilter('filterStatusVisible'));
const columns = [{
title: '磁盘名称',
dataIndex: 'diskName',
key: 'diskName',
sorter: (a: IPartitionsLocation, b: IPartitionsLocation) => a.diskName.charCodeAt(0) - b.diskName.charCodeAt(0),
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
}, {
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
sorter: (a: IPartitionsLocation, b: IPartitionsLocation) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
}, {
title: 'Leader分区',
dataIndex: 'leaderPartitions',
key: 'leaderPartitions',
onCell: () => ({
style: {
maxWidth: 250,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (value: number[]) => {
return (
<Tooltip placement="bottomLeft" title={value.join('、')}>
{value.map(i => <span key={i} className="p-params">{i}</span>)}
</Tooltip>
);
},
}, {
title: 'Follow分区',
dataIndex: 'followerPartitions',
key: 'followerPartitions',
onCell: () => ({
style: {
maxWidth: 250,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (value: number[]) => {
return (
<Tooltip placement="bottomLeft" title={value.join('、')}>
{value.map(i => <span key={i} className="p-params">{i}</span>)}
</Tooltip>
);
},
}, {
title: '未同步副本',
dataIndex: 'notUnderReplicatedPartitions',
render: (value: number[]) => {
return (
<Tooltip placement="bottomLeft" title={value.join('、')}>
{value.map(i => <span key={i} className="p-params p-params-unFinished">{i}</span>)}
</Tooltip>
);
},
},
underReplicated,
];
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入磁盘名或者Topic名称')}
</ul>
<Table
columns={columns}
expandIconColumnIndex={-1}
expandedRowRender={this.getMoreDetail}
dataSource={this.getData(admin.partitionsLocation)}
rowKey="key"
pagination={pagination}
/>
</div>
</>
);
}
public render() {
return (
admin.partitionsLocation ? <> {this.renderDiskInfo()} </> : null
);
}
}

View File

@@ -0,0 +1,139 @@
.p-params {
display: inline-block;
padding: 0px 10px;
border-radius: 4px;
border: 1px solid rgba(217, 217, 217, 1);
margin: 0px 8px 8px 0px;
&-unFinished {
background: rgba(245, 34, 45, 0.2);
}
}
.p-description {
margin-left: 20px;
span {
display: inline-block;
width: 180px;
text-align: right;
margin-right: 10px;
}
}
p.k-title {
width: 100%;
font-size: 14px;
font-family: PingFangSC-Medium;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
height: 48px;
line-height: 48px;
background: rgba(0, 0, 0, 0.02);
padding-left: 24px;
margin: 0;
}
.right-flow {
.k-abs {
right: 24px;
cursor: pointer;
& > i {
margin-right: 5px;
}
}
}
.title-flow{
width: 100%;
display: flex;
justify-content: space-around;
.k-text{
width: 420px;
line-height: 50px;
background: #00000005;
}
}
.mb-24 {
margin-bottom: 24px;
}
.k-summary {
width: 100%;
font-family: PingFangSC-Regular;
background: #fff;
.k-row-1 {
width: 100%;
height: 48px;
display: flex;
border-bottom: solid 1px #e8e8e8;
div {
flex: 1;
line-height: 48px;
padding-left: 32px;
font-size: 15px;
color: rgba(0, 0, 0, 0.85);
}
div + div {
border-left: solid 1px #e8e8e8;
}
}
.k-row-2 {
width: 100%;
padding: 24px 0;
border-top: solid 1px #e8e8e8;
border-bottom: solid 1px #e8e8e8;
display: flex;
text-align: center;
div {
height: 58px;
flex: 1;
span {
display: block;
line-height: 22px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
p {
line-height: 32px;
margin-top: 4px;
font-size: 24px;
font-weight: 400;
color: rgba(0, 0, 0, 0.85);
}
}
div + div {
border-left: solid 2px #e8e8e8;
}
}
.k-row-3 {
width: 100%;
text-align: center;
display: flex;
div {
padding: 9px 0;
flex: 1;
span {
display: block;
line-height: 22px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
p {
line-height: 22px;
margin-top: 1px;
font-size: 18px;
font-weight: 400;
color: rgba(0, 0, 0, 0.85);
}
}
.long-text {
font-size: 12px;
}
div + div {
border-left: solid 2px #e8e8e8;
}
}
}
.option-map {
min-width: 1200px;
}

View File

@@ -0,0 +1,67 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Tabs, PageHeader } from 'antd';
import { handleTabKey } from 'lib/utils';
import { IMetaData } from 'types/base-type';
import Url from 'lib/url-parser';
import { BaseInfo } from './base-info';
import { MonitorInfo } from './monitor-info';
import { TopicInfo } from './topic-info';
import { DiskInfo } from './disk-info';
import { PartitionInfo } from './partition-info';
import { TopicAnalysis } from './topic-analysis';
import { handlePageBack } from 'lib/utils';
import { admin } from 'store/admin';
const { TabPane } = Tabs;
@observer
export class BrokerDetail extends React.Component {
public clusterId: number;
public brokerId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.brokerId = Number(url.search.brokerId);
}
public componentDidMount() {
admin.getBasicInfo(this.clusterId);
}
public render() {
let content = {} as IMetaData;
content = admin.basicInfo ? admin.basicInfo : content;
return (
<>
<PageHeader
className="detail topic-detail-header"
onBack={() => handlePageBack(`/admin/cluster-detail?clusterId=${this.clusterId}#3`)}
title={`集群列表/${content.clusterName || ''}/${this.brokerId || ''}`}
/>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="基本信息" key="1">
<BaseInfo/>
</TabPane>
<TabPane tab="监控信息" key="2">
<MonitorInfo />
</TabPane>
<TabPane tab="Topic信息" key="3">
<TopicInfo tab={'Topic信息'} />
</TabPane>
<TabPane tab="磁盘信息" key="4">
<DiskInfo tab={'磁盘信息'} />
</TabPane>
<TabPane tab="partition信息" key="5">
<PartitionInfo tab={'partition信息'} />
</TabPane>
<TabPane tab="Topic分析" key="6">
<TopicAnalysis />
</TabPane>
</Tabs>
</>
);
}
}

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import Url from 'lib/url-parser';
import { adminMonitor } from 'store/admin-monitor';
import moment from 'moment';
import './index.less';
import { ExpandCard } from 'component/expand-card';
import { DataCurveFilter } from '../data-curve';
import { allCurves, ICurveType } from '../data-curve/config';
import { CommonCurve } from 'container/common-curve';
@observer
export class MonitorInfo extends React.Component {
public clusterId: number;
public brokerId: number;
public $chart: any;
public chart11: any;
public state = {
startTime: moment().subtract(1, 'hour'),
endTime: moment(),
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.brokerId = Number(url.search.brokerId);
adminMonitor.setCurrentBrokerId(this.brokerId);
adminMonitor.setCurrentClusterId(this.clusterId);
}
public handleRef(chart: any, index: number) {
this.$chart[index] = chart;
}
public getCurves = (curveType: ICurveType) => {
return curveType.curves.map(o => {
return <CommonCurve key={o.path} options={o} parser={curveType.parser} />;
});
}
public renderChart() {
return (
<div className="curve-wrapper">
<DataCurveFilter />
{allCurves.map(c => {
return <ExpandCard key={c.type} title={c.title} charts={this.getCurves(c)} />;
})}
</div>
);
}
public componentDidMount() {
adminMonitor.getBrokersMetricsList(moment().subtract(1, 'hour').format('x'), moment().format('x'));
}
public render() {
return (
<>
{adminMonitor.brokersMetricsHistory ? this.renderChart() : null}
</>
);
}
}

View File

@@ -0,0 +1,104 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Table, Tooltip } from 'component/antd';
import { columsDefault } from 'constants/status-map';
import Url from 'lib/url-parser';
import { pagination } from 'constants/table';
import { admin } from 'store/admin';
import { IBrokersPartitions } from 'types/base-type';
import './index.less';
import { SearchAndFilterContainer } from 'container/search-filter';
import { getPartitionInfoColumns } from '../config';
@observer
export class PartitionInfo extends SearchAndFilterContainer {
public clusterId: number;
public brokerId: number;
public state = {
searchKey: '',
filterStatusVisible: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.brokerId = Number(url.search.brokerId);
}
public getColumns = () => {
const columns = getPartitionInfoColumns();
const status = Object.assign({
title: '状态',
dataIndex: 'underReplicated',
key: 'underReplicated',
onCell: null,
width: '7%',
filters: [{ text: '已同步', value: true }, { text: '未同步', value: false }],
onFilter: (value: string, record: IBrokersPartitions) => record.underReplicated === Boolean(value),
render: (value: string) => <span>{Boolean(value) ? '已同步' : '未同步'}</span>,
}, this.renderColumnsFilter('filterStatusVisible'));
const col = columns.splice(4, 0, status);
return columns;
}
public getDescription = (value: any, record: any) => {
return Object.keys(value).map((key: keyof any, index: number) => {
return (
<>
<p key={index}>
<span>{value[key]}</span>
{(record[key] as []).join(',')}{(record[key] as []).length})
</p>
</>
);
});
}
public getMoreDetail = (record: IBrokersPartitions) => {
return (
<div className="p-description">
<p><span>Topic: </span>{record.topicName}</p>
<p><span>isUnderReplicated:</span>{record.underReplicated ? '已同步' : '未同步'}</p>
{this.getDescription(columsDefault, record)}
</div>
);
}
public getData<T extends IBrokersPartitions>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokersPartitions) =>
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public componentDidMount() {
admin.getBrokersPartitions(this.clusterId, this.brokerId);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Topic')}
</ul>
<Table
loading={admin.realBrokerLoading}
columns={this.getColumns()}
expandIconAsCell={true}
expandIconColumnIndex={-1}
expandedRowRender={this.getMoreDetail}
dataSource={this.getData(admin.brokersPartitions)}
rowKey="key"
pagination={pagination}
/>
</div>
);
}
}

View File

@@ -0,0 +1,117 @@
import * as React from 'react';
import { Table, Tooltip } from 'component/antd';
import { observer } from 'mobx-react';
import { admin } from 'store/admin';
import { brokerMetrics } from 'constants/status-map';
import { IBrokerHistory, IAnalysisTopicVO } from 'types/base-type';
import Url from 'lib/url-parser';
import './index.less';
const columns = [{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
},
{
title: 'Bytes In(KB/s)',
dataIndex: 'bytesInRate',
key: 'bytesInRate',
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.bytesIn) - Number(a.bytesIn),
render: (t: number, record: any) => `${record && (record.bytesIn / 1024).toFixed(2)} (${+Math.ceil((t * 100))}%)`,
},
{
title: 'Bytes Out(KB/s)',
dataIndex: 'bytesOutRate',
key: 'bytesOutRate',
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.bytesOut) - Number(a.bytesOut),
render: (t: number, record: any) => `${record && (record.bytesOut / 1024).toFixed(2)} (${+Math.ceil((t * 100))}%)`,
},
{
title: 'Message In(秒)',
dataIndex: 'messagesInRate',
key: 'messagesInRate',
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.messagesIn) - Number(a.messagesIn),
render: (t: number, record: any) => `${record && record.messagesIn} (${+Math.ceil((t * 100))}%)`,
},
{
title: 'Total Fetch Requests(秒)',
dataIndex: 'totalFetchRequestsRate',
key: 'totalFetchRequestsRate',
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.totalFetchRequests) - Number(a.totalFetchRequests),
render: (t: number, record: any) => `${record && record.totalFetchRequests} (${+Math.ceil((t * 100))}%)`,
},
{
title: 'Total Produce Requests(秒)',
dataIndex: 'totalProduceRequestsRate',
key: 'totalProduceRequestsRate',
sorter: (a: IAnalysisTopicVO, b: IAnalysisTopicVO) => Number(b.totalProduceRequests) - Number(a.totalProduceRequests),
render: (t: number, record: any) => `${record && record.totalProduceRequests} (${+Math.ceil((t * 100))}%)`,
}];
@observer
export class TopicAnalysis extends React.Component {
public clusterId: number;
public brokerId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.brokerId = Number(url.search.brokerId);
}
public brokerStatus() {
return (
<div className="k-summary">
<div className="k-row-3">
<div>
<span>Broker ID</span>
<p>{this.brokerId}</p>
</div>
{admin.brokersAnalysis ?
Object.keys(brokerMetrics).map((i: keyof IBrokerHistory) => {
return (
<div key={i}>
<span className={brokerMetrics[i].length > 25 ? 'long-text' : ''}>
{brokerMetrics[i]}</span>
<p>{(admin.brokersAnalysis[i] === null || admin.brokersAnalysis[i] === undefined) ?
'' : admin.brokersAnalysis[i].toFixed(2)}</p>
</div>
);
}) : ''}
</div>
</div>
);
}
public componentDidMount() {
admin.getBrokersAnalysis(this.clusterId, this.brokerId);
}
public render() {
let analysisTopic = [] as IAnalysisTopicVO[];
analysisTopic = admin.brokersAnalysisTopic ? admin.brokersAnalysisTopic : analysisTopic;
return(
<>
<div className="k-row right-flow mb-24">
<p className="k-title">Broker </p>
{this.brokerStatus()}
</div>
<div className="k-row right-flow">
<div className="title-flow">
<p className="k-title">Topic </p>
<span className="didi-theme k-text">Broker总量的百分比</span>
</div>
<Table
rowKey="key"
columns={columns}
dataSource={analysisTopic}
pagination={false}
/>
</div>
</>
);
}
}

View File

@@ -0,0 +1,127 @@
import * as React from 'react';
import moment from 'moment';
import { observer } from 'mobx-react';
import { pagination, cellStyle } from 'constants/table';
import { SearchAndFilterContainer } from 'container/search-filter';
import { IBrokersTopics } from 'types/base-type';
import Url from 'lib/url-parser';
import { Table, Tooltip } from 'component/antd';
import { admin } from 'store/admin';
import { region } from 'store/region';
import { timeFormat } from 'constants/strategy';
@observer
export class TopicInfo extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
public clusterId: number;
public brokerId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.brokerId = Number(url.search.brokerId);
}
public getData<T extends IBrokersTopics>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokersTopics) =>
(item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string)
|| (item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderTopicInfo() {
const cloumns = [{
title: 'Topic名称',
key: 'topicName',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
sorter: (a: IBrokersTopics, b: IBrokersTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (t: string, r: IBrokersTopics) => {
return (
<Tooltip placement="bottomLeft" title={r.topicName} >
<a
// tslint:disable-next-line:max-line-length
href={`${this.urlPrefix}/topic/topic-detail?clusterId=${this.clusterId}&topic=${r.topicName || ''}&isPhysicalClusterId=true&region=${region.currentRegion}`}
>
{r.topicName}
</a>
</Tooltip>
);
},
}, {
title: '分区数',
dataIndex: 'partitionNum',
key: 'partitionNum',
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.partitionNum - a.partitionNum,
}, {
title: '副本数',
dataIndex: 'replicaNum',
key: 'replicaNum',
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.replicaNum - a.replicaNum,
}, {
title: 'Bytes In(KB/s)',
dataIndex: 'byteIn',
key: 'byteIn',
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.byteIn - a.byteIn,
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
}, {
title: 'QPS',
dataIndex: 'produceRequest',
key: 'produceRequest',
width: '10%',
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.produceRequest - a.produceRequest,
render: (t: number) => t === null ? '' : t.toFixed(2),
}, {
title: '所属应用',
dataIndex: 'appName',
key: 'appName',
width: '10%',
render: (val: string, record: IBrokersTopics) => (
<Tooltip placement="bottomLeft" title={record.appId} >
{val}
</Tooltip>
),
}, {
title: '修改时间',
dataIndex: 'updateTime',
key: 'updateTime',
sorter: (a: IBrokersTopics, b: IBrokersTopics) => b.updateTime - a.updateTime,
render: (t: number) => moment(t).format(timeFormat),
}];
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Topic名称或者负责人')}
</ul>
<Table columns={cloumns} dataSource={this.getData(admin.brokersTopics)} rowKey="key" pagination={pagination} />
</div>
</>
);
}
public componentDidMount() {
admin.getBrokersTopics(this.clusterId, this.brokerId);
}
public render() {
return (
admin.brokersTopics ? <> {this.renderTopicInfo()} </> : null
);
}
}

View File

@@ -0,0 +1,259 @@
import * as React from 'react';
import { Table, notification, Button, Divider, Popconfirm } from 'component/antd';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import { IBrokerData, IEnumsMap, IMetaData } from 'types/base-type';
import { admin } from 'store/admin';
import { tableFilter, transBToMB } from 'lib/utils';
import { SearchAndFilterContainer } from 'container/search-filter';
import { DoughnutChart } from 'component/chart';
import { LeaderRebalanceWrapper } from 'container/modal/admin/leader-rebalance';
import { timeFormat } from 'constants/strategy';
import Url from 'lib/url-parser';
import moment from 'moment';
import './index.less';
@observer
export class ClusterBroker extends SearchAndFilterContainer {
public clusterId: number;
public clusterName: string;
public state = {
searchKey: '',
filterPeakFlowVisible: false,
filterReplicatedVisible: false,
filterRegionVisible: false,
filterStatusVisible: false,
reblanceVisible: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderBrokerData(clusterBroker: IBrokerData[]) {
let peakFlow = [] as IEnumsMap[];
peakFlow = admin.peakFlowStatusList ? admin.peakFlowStatusList : peakFlow;
const peakFlowStatus = Object.assign({
title: '峰值状态',
dataIndex: 'peakFlowStatus',
key: 'peakFlowStatus',
width: '8%',
filters: peakFlow.map(ele => ({ text: ele.message, value: ele.code + '' })),
onFilter: (value: string, record: IBrokerData) => record.peakFlowStatus === +value,
render: (value: number) => {
let messgae: string;
peakFlow.map(ele => {
if (ele.code === value) {
messgae = ele.message;
}
});
return (
<span>{messgae}</span>
);
},
}, this.renderColumnsFilter('filterPeakFlowVisible'));
const underReplicated = Object.assign({
title: '副本状态',
dataIndex: 'underReplicated',
key: 'underReplicated',
width: '8%',
filters: [{ text: '同步', value: 'false' }, { text: '未同步', value: 'true' }],
onFilter: (value: string, record: IBrokerData) => record.underReplicated === (value === 'true') ? true : false,
render: (t: boolean) => t !== null ? <span className={t ? 'fail' : 'success'}>{t ? '未同步' : '同步'}</span> : '',
}, this.renderColumnsFilter('filterReplicatedVisible'));
const region = Object.assign({
title: 'regionName',
dataIndex: 'regionName',
key: 'regionName',
width: '10%',
filters: tableFilter<any>(clusterBroker, 'regionName'),
onFilter: (value: string, record: IBrokerData) => record.regionName === value,
}, this.renderColumnsFilter('filterRegionVisible'));
const status = Object.assign({
title: '状态',
dataIndex: 'status',
key: 'status',
width: '8%',
filters: [{ text: '未使用', value: '-1' }, { text: '使用中', value: '0' }],
onFilter: (value: string, record: IBrokerData) => record.status === Number(value),
render: (t: number) => <span className={t === 0 ? 'success' : 'fail'}>{t === 0 ? '使用中' : '未使用'}</span>,
}, this.renderColumnsFilter('filterStatusVisible'));
const columns = [
{
title: 'ID',
dataIndex: 'brokerId',
key: 'brokerId',
width: '5%',
sorter: (a: IBrokerData, b: IBrokerData) => b.brokerId - a.brokerId,
render: (text: number, record: IBrokerData) => {
// tslint:disable-next-line:max-line-length
const query = `clusterId=${this.clusterId}&brokerId=${record.brokerId}`;
const judge = record.underReplicated === false && record.status !== 0;
return (
<span className={judge ? 'fail' : ''}>
{
// tslint:disable-next-line:max-line-length
record.status === 0 ? <a href={`${this.urlPrefix}/admin/broker-detail?${query}`}>{text}</a>
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{text}</a>}
</span>);
},
},
{
title: '主机',
dataIndex: 'host',
key: 'host',
width: '10%',
sorter: (a: any, b: any) => a.host.charCodeAt(0) - b.host.charCodeAt(0),
},
{
title: 'Port',
dataIndex: 'port',
key: 'port',
width: '6%',
sorter: (a: IBrokerData, b: IBrokerData) => b.port - a.port,
},
{
title: 'JMX Port',
dataIndex: 'jmxPort',
key: 'jmxPort',
width: '7%',
sorter: (a: IBrokerData, b: IBrokerData) => b.jmxPort - a.jmxPort,
},
{
title: '启动时间',
dataIndex: 'startTime',
key: 'startTime',
width: '10%',
sorter: (a: IBrokerData, b: IBrokerData) => b.startTime - a.startTime,
render: (time: number) => moment(time).format(timeFormat),
},
{
title: 'Bytes InMB/s',
dataIndex: 'byteIn',
key: 'byteIn',
width: '10%',
sorter: (a: IBrokerData, b: IBrokerData) => b.byteIn - a.byteIn,
render: (t: number) => transBToMB(t),
},
{
title: 'Bytes OutMB/s',
dataIndex: 'byteOut',
key: 'byteOut',
width: '10%',
sorter: (a: IBrokerData, b: IBrokerData) => b.byteOut - a.byteOut,
render: (t: number) => transBToMB(t),
},
peakFlowStatus,
underReplicated,
region,
status,
{
title: '操作',
width: '10%',
render: (text: string, record: IBrokerData) => {
// tslint:disable-next-line:max-line-length
const query = `clusterId=${this.clusterId}&brokerId=${record.brokerId}`;
return ( // 0 监控中 可点击详情,不可删除 -1 暂停监控 不可点击详情,可删除
<>
{record.status === 0 ?
<a href={`${this.urlPrefix}/admin/broker-detail?${query}`} className="action-button"></a>
: <a style={{ cursor: 'not-allowed', color: '#999' }}></a>}
<Popconfirm
title="确定删除?"
onConfirm={() => this.deteleTopic(record)}
disabled={record.status === 0}
>
<a style={record.status === 0 ? { cursor: 'not-allowed', color: '#999' } : {}}>
</a>
</Popconfirm>
</>
);
},
},
];
return (
<Table dataSource={clusterBroker} columns={columns} pagination={pagination} />
);
}
public deteleTopic(record: any) {
admin.deteleClusterBrokers(this.clusterId, record.brokerId).then(data => {
notification.success({ message: '删除成功' });
});
}
public reblanceInfo() {
this.setState({ reblanceVisible: true });
}
public handleVisible(val: boolean) {
this.setState({ reblanceVisible: val });
}
public async componentDidMount() {
await admin.getPeakFlowStatus();
admin.getClusterBroker(this.clusterId);
admin.getBrokersMetadata(this.clusterId);
admin.getBrokersStatus(this.clusterId);
}
public getData<T extends IBrokerData>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokerData) =>
(item.brokerId !== undefined && item.brokerId !== null) && (item.brokerId + '').toLowerCase().includes(searchKey as string)
|| (item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public render() {
const content = this.props.basicInfo as IMetaData;
if (content) {
this.clusterName = content.clusterName;
}
return (
<>
<div className="diagram">
<div className="diagram-box">
<h2>使</h2>
<Divider className="hotspot-divider" />
{admin.peakValueList.length ? <DoughnutChart
getChartData={() => admin.getPeakFlowChartData(admin.peakValueList, admin.peakValueMap)}
/> : null}
</div>
<div className="diagram-box">
<h2></h2>
<Divider className="hotspot-divider" />
{admin.copyValueList.length ? <DoughnutChart
getChartData={() => admin.getSideStatusChartData(admin.copyValueList)}
/> : null}
</div>
</div>
<div className="leader-seacrh">
<div className="search-top">
{this.renderSearch('', '请输入ID或主机')}
<Button onClick={() => this.reblanceInfo()} type="primary">Leader Rebalance</Button>
</div>
{this.renderBrokerData(this.getData(admin.clusterBroker))}
</div>
{ this.state.reblanceVisible && <LeaderRebalanceWrapper
changeVisible={(val: boolean) => this.handleVisible(val)}
visible={this.state.reblanceVisible}
clusterId={this.clusterId}
clusterName={this.clusterName}
/> }
</>
);
}
}

View File

@@ -0,0 +1,131 @@
import * as React from 'react';
import { Table, Modal, Tooltip } from 'component/antd';
import { observer } from 'mobx-react';
import Url from 'lib/url-parser';
import { IOffset, IXFormWrapper } from 'types/base-type';
import { SearchAndFilterContainer } from 'container/search-filter';
import { pagination } from 'constants/table';
import { admin } from 'store/admin';
import { getConsumerDetails } from 'lib/api';
import './index.less';
@observer
export class ClusterConsumer extends SearchAndFilterContainer {
public clusterId: number;
public consumerDetails = [] as string[];
public state = {
searchKey: '',
detailsVisible: false,
};
public columns = [{
title: '消费组名称',
dataIndex: 'consumerGroup',
key: 'consumerGroup',
width: '70%',
sorter: (a: IOffset, b: IOffset) => a.consumerGroup.charCodeAt(0) - b.consumerGroup.charCodeAt(0),
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
}, {
title: 'Location',
dataIndex: 'location',
key: 'location',
width: '20%',
render: (t: string) => t.toLowerCase(),
}, {
title: '操作',
key: 'operation',
width: '10%',
render: (t: string, item: IOffset) => {
return (<a onClick={() => this.getConsumeDetails(item)}></a>);
},
}];
private xFormModal: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getConsumeDetails(record: IOffset) {
getConsumerDetails(this.clusterId, record.consumerGroup, record.location).then((data: string[]) => {
this.consumerDetails = data;
this.setState({ detailsVisible: true });
});
}
public handleDetailsOk() {
this.setState({ detailsVisible: false });
}
public handleDetailsCancel() {
this.setState({ detailsVisible: false });
}
public getData<T extends IOffset>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IOffset) =>
(item.consumerGroup !== undefined && item.consumerGroup !== null) && item.consumerGroup.toLowerCase().includes(searchKey as string)
|| (item.location !== undefined && item.location !== null) && item.location.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public componentDidMount() {
admin.getClusterConsumer(this.clusterId);
}
public render() {
let details: any[];
details = this.consumerDetails ? this.consumerDetails.map((ele, index) => {
return {
key: index,
topicName: ele,
};
}) : [];
const consumptionColumns = [{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
}];
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch()}
</ul>
<Table
columns={this.columns}
dataSource={this.getData(admin.consumerData)}
pagination={pagination}
rowKey="key"
/>
</div>
<Modal
title="消费的Topic"
visible={this.state.detailsVisible}
onOk={() => this.handleDetailsOk()}
onCancel={() => this.handleDetailsCancel()}
maskClosable={false}
footer={null}
>
<Table
columns={consumptionColumns}
dataSource={details}
pagination={pagination}
rowKey="key"
scroll={{ y: 260 }}
/>
</Modal>
</>
);
}
}

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { Table } from 'component/antd';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import Url from 'lib/url-parser';
import { IController } from 'types/base-type';
import { admin } from 'store/admin';
import './index.less';
import moment from 'moment';
import { timeFormat } from 'constants/strategy';
@observer
export class ClusterController extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getData<T extends IController>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IController) =>
(item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderController() {
const columns = [
{
title: 'BrokerId',
dataIndex: 'brokerId',
key: 'brokerId',
width: '30%',
sorter: (a: IController, b: IController) => b.brokerId - a.brokerId,
},
{
title: 'BrokerHost',
key: 'host',
dataIndex: 'host',
width: '30%',
render: (r: string, t: IController) => {
return (
<a href={`${this.urlPrefix}/admin/broker-detail?clusterId=${this.clusterId}&brokerId=${t.brokerId}`}>{r}
</a>
);
},
},
{
title: '变更时间',
dataIndex: 'timestamp',
key: 'timestamp',
width: '40%',
sorter: (a: IController, b: IController) => b.timestamp - a.timestamp,
render: (t: number) => moment(t).format(timeFormat),
},
];
return (
<Table
columns={columns}
dataSource={this.getData(admin.controllerHistory)}
pagination={pagination}
rowKey="key"
/>
);
}
public componentDidMount() {
admin.getControllerHistory(this.clusterId);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Host')}
</ul>
{this.renderController()}
</div>
);
}
}

View File

@@ -0,0 +1,141 @@
import * as React from 'react';
import { PageHeader, Descriptions, Divider, Tooltip, Icon, Spin } from 'component/antd';
import { ILabelValue, IMetaData, IOptionType, IClusterReal } from 'types/base-type';
import { controlOptionMap, clusterTypeMap } from 'constants/status-map';
import { copyString } from 'lib/utils';
import { observer } from 'mobx-react';
import { admin } from 'store/admin';
import Url from 'lib/url-parser';
import moment from 'moment';
import './index.less';
import { StatusGraghCom } from 'component/flow-table';
import { renderTrafficTable, NetWorkFlow } from 'container/network-flow';
import { timeFormat } from 'constants/strategy';
interface IOverview {
basicInfo: IMetaData;
}
@observer
export class ClusterOverview extends React.Component<IOverview> {
public clusterId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public clusterContent() {
const content = this.props.basicInfo as IMetaData;
const gmtCreate = moment(content.gmtCreate).format(timeFormat);
const clusterContent = [{
value: content.clusterName,
label: '集群名称',
}, {
value: clusterTypeMap[content.mode],
label: '集群类型',
}, {
value: gmtCreate,
label: '接入时间',
}];
const clusterInfo = [{
value: content.kafkaVersion,
label: 'kafka版本',
}, {
value: content.bootstrapServers,
label: 'Bootstrap Severs',
}, {
value: content.zookeeper,
label: 'Zookeeper',
}];
return (
<>
<div className="chart-title"></div>
<PageHeader className="detail" title="">
<Descriptions size="small" column={3}>
{clusterContent.map((item: ILabelValue, index: number) => (
<Descriptions.Item
key={index}
label={item.label}
>{item.value}
</Descriptions.Item>
))}
{clusterInfo.map((item: ILabelValue, index: number) => (
<Descriptions.Item key={index} label={item.label}>
<Tooltip placement="bottomLeft" title={item.value}>
<span className="overview-bootstrap">
<Icon
onClick={() => copyString(item.value)}
type="copy"
className="didi-theme overview-theme"
/>
<i className="overview-boot">{item.value}</i>
</span>
</Tooltip>
</Descriptions.Item>
))}
</Descriptions>
</PageHeader>
</>
);
}
public updateRealStatus = () => {
admin.getClusterRealTime(this.clusterId);
}
public onSelectChange(e: IOptionType) {
return admin.changeType(e);
}
public getOptionApi = () => {
return admin.getClusterMetrice(this.clusterId);
}
public componentDidMount() {
admin.getClusterRealTime(this.clusterId);
}
public renderHistoryTraffic() {
return (
<NetWorkFlow
key="1"
selectArr={controlOptionMap}
type={admin.type}
selectChange={(value: IOptionType) => this.onSelectChange(value)}
getApi={() => this.getOptionApi()}
/>
);
}
public renderTrafficInfo = () => {
return (
<Spin spinning={admin.realClusterLoading}>
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
</Spin>
);
}
public render() {
return (
<>
<div className="base-info">
{this.clusterContent()}
{this.renderTrafficInfo()}
{this.renderHistoryTraffic()}
</div>
</>
);
}
}
@observer
export class StatusGragh extends StatusGraghCom<IClusterReal> {
public getData = () => {
return admin.clusterRealData;
}
public getLoading = () => {
return admin.realClusterLoading;
}
}

View File

@@ -0,0 +1,217 @@
import * as React from 'react';
import Url from 'lib/url-parser';
import { region } from 'store';
import { admin } from 'store/admin';
import { Table, notification, Tooltip, Popconfirm } from 'antd';
import { pagination, cellStyle } from 'constants/table';
import { observer } from 'mobx-react';
import { IClusterTopics } from 'types/base-type';
import { deleteClusterTopic } from 'lib/api';
import { SearchAndFilterContainer } from 'container/search-filter';
import { users } from 'store/users';
import { urlPrefix } from 'constants/left-menu';
import { transMSecondToHour } from 'lib/utils';
import './index.less';
import moment = require('moment');
import { ExpandPartitionFormWrapper } from 'container/modal/admin/expand-partition';
import { showEditClusterTopic } from 'container/modal/admin';
import { timeFormat } from 'constants/strategy';
@observer
export class ClusterTopic extends SearchAndFilterContainer {
public clusterId: number;
public clusterTopicsFrom: IClusterTopics;
public state = {
searchKey: '',
expandVisible: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getBaseInfo(item: IClusterTopics) {
admin.getTopicsBasicInfo(item.clusterId, item.topicName).then(data => {
showEditClusterTopic(data);
});
}
public handleVisible(val: boolean) {
this.setState({ expandVisible: val });
}
public expandPartition(item: IClusterTopics) {
this.clusterTopicsFrom = item;
this.setState({
expandVisible: true,
});
}
public deleteTopic(item: IClusterTopics) {
const value = [{
clusterId: item.clusterId,
topicName: item.topicName,
unForce: false,
}];
deleteClusterTopic(value).then(data => {
notification.success({ message: '删除成功' });
admin.getClusterTopics(this.clusterId);
});
}
public getData<T extends IClusterTopics>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IClusterTopics) =>
(item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string)
|| (item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public componentDidMount() {
admin.getClusterTopics(this.clusterId);
}
public renderClusterTopicList() {
const clusterColumns = [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
width: '15%',
sorter: (a: IClusterTopics, b: IClusterTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (text: string, record: IClusterTopics) => {
return (
<Tooltip placement="bottomLeft" title={record.topicName} >
<a
// tslint:disable-next-line:max-line-length
href={`${urlPrefix}/topic/topic-detail?clusterId=${record.clusterId || ''}&topic=${record.topicName || ''}&isPhysicalClusterId=true&region=${region.currentRegion}`}
>
{text}
</a>
</Tooltip>);
},
},
{
title: 'QPS',
dataIndex: 'produceRequest',
key: 'produceRequest',
width: '10%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.produceRequest - a.produceRequest,
render: (t: number) => t === null ? '' : t.toFixed(2),
},
{
title: 'Bytes In(KB/s)',
dataIndex: 'byteIn',
key: 'byteIn',
width: '15%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.byteIn - a.byteIn,
render: (t: number) => t === null ? '' : (t / 1024).toFixed(2),
},
{
title: '所属应用',
dataIndex: 'appName',
key: 'appName',
width: '10%',
render: (val: string, record: IClusterTopics) => (
<Tooltip placement="bottomLeft" title={record.appId} >
{val}
</Tooltip>
),
},
{
title: '保存时间(h)',
dataIndex: 'retentionTime',
key: 'retentionTime',
width: '10%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.retentionTime - a.retentionTime,
render: (time: any) => transMSecondToHour(time),
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
render: (t: number) => moment(t).format(timeFormat),
width: '10%',
},
{
title: 'Topic说明',
dataIndex: 'description',
key: 'description',
width: '15%',
onCell: () => ({
style: {
maxWidth: 180,
...cellStyle,
},
}),
},
{
title: '操作',
width: '30%',
render: (value: string, item: IClusterTopics) => (
<>
<a onClick={() => this.getBaseInfo(item)} className="action-button"></a>
<a onClick={() => this.expandPartition(item)} className="action-button"></a>
<Popconfirm
title="确定删除?"
onConfirm={() => this.deleteTopic(item)}
>
<a></a>
</Popconfirm>
</>
),
},
];
if (users.currentUser.role !== 2) {
clusterColumns.splice(-1, 1);
}
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Topic名称应用名称')}
</ul>
<Table
loading={admin.loading}
rowKey="key"
dataSource={this.getData(admin.clusterTopics)}
columns={clusterColumns}
pagination={pagination}
/>
</div>
{this.renderExpandModal()}
</>
);
}
public renderExpandModal() {
let formData = {} as IClusterTopics;
formData = this.clusterTopicsFrom ? this.clusterTopicsFrom : formData;
return (
<>
{this.state.expandVisible && <ExpandPartitionFormWrapper
handleVisible={(val: boolean) => this.handleVisible(val)}
visible={this.state.expandVisible}
formData={formData}
clusterId={this.clusterId}
/>}
</>
);
}
public render() {
return (
admin.clusterTopics ? <> {this.renderClusterTopicList()} </> : null
);
}
}

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { Table, Tooltip } from 'component/antd';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import Url from 'lib/url-parser';
import { IThrottles } from 'types/base-type';
import { admin } from 'store/admin';
import './index.less';
@observer
export class CurrentLimiting extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderController() {
const clientType = Object.assign({
title: '类型',
dataIndex: 'throttleClientType',
key: 'throttleClientType',
width: '15%',
filters: [{ text: 'fetch', value: 'Fetch' }, { text: 'produce', value: 'Produce' }],
onFilter: (value: string, record: IThrottles) => record.throttleClientType === value,
render: (t: string) => t,
}, this.renderColumnsFilter('filterStatus'));
const columns = [
{
title: 'Topic名称',
key: 'topicName',
dataIndex: 'topicName',
width: '50%',
sorter: (a: IThrottles, b: IThrottles) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
width: '15%',
sorter: (a: IThrottles, b: IThrottles) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
},
clientType,
{
title: 'Broker',
dataIndex: 'brokerIdList',
key: 'brokerIdList',
width: '20%',
render: (value: number[]) => {
const num = value ? `[${value.join(',')}]` : '';
return(
<span>{num}</span>
);
},
},
];
const { searchKey } = this.state;
if (!admin.clustersThrottles ) return null;
const clustersThrottles = admin.clustersThrottles.filter(d =>
((d.topicName !== undefined && d.topicName !== null) && d.topicName.toLowerCase().includes(searchKey.toLowerCase()))
|| ((d.appId !== undefined && d.appId !== null) && d.appId.toLowerCase().includes(searchKey.toLowerCase())));
return (
<Table
columns={columns}
dataSource={clustersThrottles}
pagination={pagination}
rowKey="key"
/>
);
}
public componentDidMount() {
admin.getClustersThrottles(this.clusterId);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch()}
</ul>
{this.renderController()}
</div>
);
}
}

View File

@@ -0,0 +1,291 @@
import * as React from 'react';
import { Table, notification, Tooltip, Popconfirm } from 'component/antd';
import { observer } from 'mobx-react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { pagination, cellStyle } from 'constants/table';
import { wrapper } from 'store';
import { admin } from 'store/admin';
import { IXFormWrapper, IBrokersRegions, INewRegions, IMetaData } from 'types/base-type';
import { deleteRegions } from 'lib/api';
import { transBToMB } from 'lib/utils';
import Url from 'lib/url-parser';
import moment from 'moment';
import './index.less';
import { timeFormat } from 'constants/strategy';
@observer
export class ExclusiveCluster extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
filterStatus: false,
};
private xFormModal: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderColumns = () => {
const status = Object.assign({
title: '状态',
dataIndex: 'status',
key: 'status',
width: '10%',
filters: [{ text: '正常', value: '0' }, { text: '容量已满', value: '1' }],
onFilter: (value: string, record: IBrokersRegions) => record.status === Number(value),
render: (t: number) => <span className={t === 0 ? 'success' : 'fail'}>{t === 0 ? '正常' : '容量已满'}</span>,
}, this.renderColumnsFilter('filterStatus'));
return [
{
title: 'RegionID',
dataIndex: 'id',
key: 'id',
width: '7%',
},
{
title: 'Region名称',
dataIndex: 'name',
key: 'name',
width: '13%',
onCell: () => ({
style: {
maxWidth: 160,
...cellStyle,
},
}),
sorter: (a: IBrokersRegions, b: IBrokersRegions) => a.name.charCodeAt(0) - b.name.charCodeAt(0),
render: (text: string, r: IBrokersRegions) => (
<Tooltip placement="bottomLeft" title={text}>
{text}
</Tooltip>),
},
{
title: 'BrokerIdList',
dataIndex: 'brokerIdList',
key: 'brokerIdList',
width: '10%',
onCell: () => ({
style: {
maxWidth: 200,
...cellStyle,
},
}),
render: (value: number[]) => {
const num = value ? value.join(',') : '';
return(
<Tooltip placement="bottomLeft" title={num}>
{num}
</Tooltip>);
},
},
{
title: '预估容量MB/s',
dataIndex: 'capacity',
key: 'capacity',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.capacity - a.capacity,
render: (t: number) => transBToMB(t),
},
{
title: '实际流量MB/s',
dataIndex: 'realUsed',
key: 'realUsed',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.realUsed - a.realUsed,
render: (t: number) => transBToMB(t),
},
{
title: '预估流量MB/s',
dataIndex: 'estimateUsed',
key: 'estimateUsed',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.estimateUsed - a.estimateUsed,
render: (t: number) => transBToMB(t),
},
{
title: '修改时间',
dataIndex: 'gmtModify',
key: 'gmtModify',
width: '10%',
sorter: (a: IBrokersRegions, b: IBrokersRegions) => b.gmtModify - a.gmtModify,
render: (t: number) => moment(t).format(timeFormat),
},
status,
{
title: '备注',
dataIndex: 'description',
key: 'description',
width: '10%',
onCell: () => ({
style: {
maxWidth: 200,
...cellStyle,
},
}),
render: (text: string, r: IBrokersRegions) => (
<Tooltip placement="bottomLeft" title={text}>
{text}
</Tooltip>),
},
{
title: '操作',
width: '10%',
render: (text: string, record: IBrokersRegions) => {
return (
<span className="table-operation">
<a onClick={() => this.addOrModifyRegion(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => this.handleDeleteRegion(record)}
>
<a></a>
</Popconfirm>
</span>
);
},
},
];
}
public handleDeleteRegion = (record: IBrokersRegions) => {
deleteRegions(record.id).then(() => {
notification.success({ message: '删除成功' });
admin.getBrokersRegions(this.clusterId);
});
}
public addOrModifyRegion(record?: IBrokersRegions) {
const content = this.props.basicInfo as IMetaData;
this.xFormModal = {
formMap: [
{
key: 'name',
label: 'Region名称',
rules: [{ required: true, message: '请输入Region名称' }],
attrs: { placeholder: '请输入Region名称' },
},
{
key: 'clusterName',
label: '集群名称',
rules: [{ required: true, message: '请输入集群名称' }],
defaultValue: content.clusterName,
attrs: {
disabled: true,
placeholder: '请输入集群名称',
},
},
{
key: 'brokerIdList',
label: 'Broker列表',
defaultValue: record ? record.brokerIdList.join(',') : [],
rules: [{ required: true, message: '请输入BrokerIdList' }],
attrs: {
placeholder: '请输入BrokerIdList',
},
},
{
key: 'status',
label: '状态',
type: 'select',
options: [
{
label: '正常',
value: 0,
},
{
label: '容量已满',
value: 1,
},
],
defaultValue: 0,
rules: [{ required: true, message: '请选择状态' }],
attrs: {
placeholder: '请选择状态',
},
},
{
key: 'description',
label: '备注',
type: 'text_area',
rules: [{
required: false,
}],
attrs: {
placeholder: '请输入备注',
},
},
],
formData: record,
visible: true,
title: `${record ? '编辑' : '新增Region'}`,
onSubmit: (value: INewRegions) => {
value.clusterId = this.clusterId;
value.brokerIdList = value.brokerIdList && Array.isArray(value.brokerIdList) ?
value.brokerIdList : value.brokerIdList.split(',');
if (record) {
value.id = record.id;
}
delete value.clusterName;
if (record) {
return admin.editRegions(this.clusterId, value).then(data => {
notification.success({ message: '编辑Region成功' });
});
}
return admin.addNewRegions(this.clusterId, value).then(data => {
notification.success({ message: '新建Region成功' });
});
},
};
wrapper.open(this.xFormModal);
}
public componentDidMount() {
admin.getBrokersRegions(this.clusterId);
admin.getBrokersMetadata(this.clusterId);
}
public getData<T extends IBrokersRegions>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokersRegions) =>
(item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderRegion() {
return (
<Table
columns={this.renderColumns()}
dataSource={this.getData(admin.brokersRegions)}
pagination={pagination}
rowKey="id"
/>
);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
<li className="k-add" onClick={() => this.addOrModifyRegion()}>
<i className="k-icon-xinjian didi-theme" />
<span>Region</span>
</li>
{this.renderSearch('', '请输入Region名称')}
</ul>
{this.renderRegion()}
</div>
);
}
}

View File

@@ -0,0 +1,84 @@
.table-operation-bar {
position: absolute;
right: 24px;
z-index: 100;
li {
display: inline-block;
vertical-align: middle;
.ant-select {
width: 150px;
}
.ant-input-search {
width: 200px;
}
}
}
.traffic-table {
margin: 10px 0;
min-height: 450px;
.traffic-header {
width: 100%;
height: 44px;
font-weight: bold;
background: rgb(245, 245, 245);
border: 1px solid #e8e8e8;
padding: 0 10px;
display: flex;
justify-content: space-between;
span {
color: rgba(0, 0, 0, 0.65);
font-size: 13px;
line-height: 44px;
font-weight: 100;
}
.k-abs {
font-size: 12px;
}
}
}
.implement-button {
float: right;
margin-right: -120px;
}
.diagram {
min-width: 900px;
background: white;
height: 400px;
display: flex;
justify-content: space-around;
.diagram-box {
float: left;
margin-right: 10px;
width: 46%;
height: 100%;
h2 {
line-height: 30px;
font-size: 13px;
margin-left: 15px;
margin-top: 10px;
}
}
}
.descriptions {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.leader-seacrh {
z-index: 9999999;
margin-top: 10px;
.search-top {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
}

View File

@@ -0,0 +1,73 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Tabs, PageHeader } from 'antd';
import { IMetaData } from 'types/base-type';
import { ClusterOverview } from './cluster-overview';
import { ClusterTopic } from './cluster-topic';
import { ClusterBroker } from './cluster-broker';
import { ClusterConsumer } from './cluster-consumer';
import { ExclusiveCluster } from './exclusive-cluster';
import { LogicalCluster } from './logical-cluster';
import { ClusterController } from './cluster-controller';
import { CurrentLimiting } from './current-limiting';
import { handleTabKey } from 'lib/utils';
import { admin } from 'store/admin';
import { handlePageBack } from 'lib/utils';
import Url from 'lib/url-parser';
const { TabPane } = Tabs;
@observer
export class ClusterDetail extends React.Component {
public clusterId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public componentDidMount() {
admin.getBasicInfo(this.clusterId);
}
public render() {
let content = {} as IMetaData;
content = admin.basicInfo ? admin.basicInfo : content;
return (
<>
<PageHeader
className="detail topic-detail-header"
onBack={() => handlePageBack('/admin')}
title={`集群列表/${content.clusterName || ''}`}
/>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="集群概览" key="1">
<ClusterOverview basicInfo={content} />
</TabPane>
<TabPane tab="Topic信息" key="2">
<ClusterTopic tab={'Topic信息'}/>
</TabPane>
<TabPane tab="Broker信息" key="3">
<ClusterBroker tab={'Broker信息'} basicInfo={content} />
</TabPane>
<TabPane tab="消费组信息" key="4">
<ClusterConsumer tab={'消费组信息'} />
</TabPane>
<TabPane tab="Region信息" key="5">
<ExclusiveCluster tab={'Region信息'} basicInfo={content} />
</TabPane>
<TabPane tab="逻辑集群信息" key="6">
<LogicalCluster tab={'逻辑集群信息'} basicInfo={content} />
</TabPane>
<TabPane tab="Controller变更历史" key="7">
<ClusterController tab={'Controller变更历史'} />
</TabPane>
<TabPane tab="限流信息" key="8">
<CurrentLimiting tab={'限流信息'}/>
</TabPane>
</Tabs>
</>
);
}
}

View File

@@ -0,0 +1,169 @@
import * as React from 'react';
import { Table, notification, Popconfirm } from 'component/antd';
import { observer } from 'mobx-react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { pagination } from 'constants/table';
import Url from 'lib/url-parser';
import moment from 'moment';
import { admin } from 'store/admin';
import { cluster } from 'store/cluster';
import { ILogicalCluster } from 'types/base-type';
import './index.less';
import { app } from 'store/app';
import { showLogicalClusterOpModal } from 'container/modal';
import { timeFormat } from 'constants/strategy';
@observer
export class LogicalCluster extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
filterStatus: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public renderColumns = () => {
return [
{
title: '逻辑集群ID',
dataIndex: 'logicalClusterId',
key: 'logicalClusterId',
},
{
title: '逻辑集群名称',
dataIndex: 'logicalClusterName',
key: 'logicalClusterName',
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
},
{
title: 'RegionIdList',
dataIndex: 'regionIdList',
key: 'regionIdList',
render: (value: number[]) => {
const num = value ? `[${value.join(',')}]` : '';
return(
<span>{num}</span>
);
},
},
{
title: '集群模式',
dataIndex: 'mode',
key: 'mode',
render: (value: number) => {
let val = '';
cluster.clusterModes.forEach((ele: any) => {
if (value === ele.code) {
val = ele.message;
}
});
return(<span>{val}</span>);
},
},
{
title: '修改时间',
dataIndex: 'gmtModify',
key: 'gmtModify',
render: (t: number) => moment(t).format(timeFormat),
},
{
title: '备注',
dataIndex: 'description',
key: 'description',
},
{
title: '操作',
render: (text: string, record: ILogicalCluster) => {
return (
<span className="table-operation">
<a onClick={() => this.editRegion(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => this.handleDeleteRegion(record)}
>
<a></a>
</Popconfirm>
</span>
);
},
},
];
}
public handleDeleteRegion = (record: ILogicalCluster) => {
admin.deteleLogicalClusters(this.clusterId, record.logicalClusterId).then(() => {
notification.success({ message: '删除成功' });
});
}
public async editRegion(record: ILogicalCluster) {
await admin.queryLogicalClusters(record.logicalClusterId);
await this.addOrEditLogicalCluster(admin.queryLogical);
}
public addOrEditLogicalCluster(record?: ILogicalCluster) {
showLogicalClusterOpModal(this.clusterId, record);
}
public componentDidMount() {
admin.getLogicalClusters(this.clusterId);
cluster.getClusterModes();
admin.getBrokersRegions(this.clusterId);
if (!app.adminAppData.length) {
app.getAdminAppList();
}
}
public getData<T extends ILogicalCluster>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: ILogicalCluster) =>
(item.logicalClusterName !== undefined && item.logicalClusterName !== null)
&& item.logicalClusterName.toLowerCase().includes(searchKey as string)
|| (item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderLogicalCluster() {
return (
<Table
columns={this.renderColumns()}
dataSource={this.getData(admin.logicalClusters)}
pagination={pagination}
rowKey="key"
/>
);
}
public render() {
return (
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
<li className="k-add" onClick={() => this.addOrEditLogicalCluster()}>
<i className="k-icon-xinjian didi-theme" />
<span></span>
</li>
{this.renderSearch('', '请输入逻辑集群名称或AppId')}
</ul>
{this.renderLogicalCluster()}
</div>
);
}
}

View File

@@ -0,0 +1,318 @@
import * as React from 'react';
import { Modal, Table, Button, notification, message, Tooltip, Icon, Popconfirm } from 'component/antd';
import { wrapper } from 'store';
import { observer } from 'mobx-react';
import { IXFormWrapper, IMetaData, IRegister } from 'types/base-type';
import { admin } from 'store/admin';
import { registerCluster, createCluster, pauseMonitoring } from 'lib/api';
import { SearchAndFilterContainer } from 'container/search-filter';
import { cluster } from 'store/cluster';
import { customPagination } from 'constants/table';
import { urlPrefix } from 'constants/left-menu';
import { region } from 'store';
import './index.less';
import { getAdminClusterColumns } from '../config';
const { confirm } = Modal;
@observer
export class ClusterList extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
private xFormModal: IXFormWrapper;
// TODO: 公共化
public renderClusterHref(value: number | string, item: IMetaData, key: number) {
return ( // 0 暂停监控--不可点击 1 监控中---可正常点击
<>
{item.status === 1 ? <a href={`${urlPrefix}/admin/cluster-detail?clusterId=${item.clusterId}#${key}`}>{value}</a>
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{value}</a>}
</>
);
}
public createOrRegisterCluster(item: IMetaData) {
this.xFormModal = {
formMap: [
{
key: 'clusterName',
label: '集群名称',
rules: [{
required: true,
message: '请输入集群名称',
}],
attrs: {
placeholder: '请输入集群名称',
disabled: item ? true : false,
},
},
{
key: 'zookeeper',
label: 'zookeeper地址',
type: 'text_area',
rules: [{
required: true,
message: '请输入zookeeper地址',
}],
attrs: {
placeholder: '请输入zookeeper地址',
rows: 2,
disabled: item ? true : false,
},
},
{
key: 'bootstrapServers',
label: 'bootstrapServers',
type: 'text_area',
rules: [{
required: true,
message: '请输入bootstrapServers',
}],
attrs: {
placeholder: '请输入bootstrapServers',
rows: 2,
disabled: item ? true : false,
},
},
{
key: 'idc',
label: '数据中心',
defaultValue: region.regionName,
rules: [{ required: true, message: '请输入数据中心' }],
attrs: {
placeholder: '请输入数据中心',
disabled: true,
},
},
{
key: 'mode',
label: '集群类型',
type: 'select',
options: cluster.clusterModes.map(ele => {
return {
label: ele.message,
value: ele.code,
};
}),
rules: [{
required: true,
message: '请选择集群类型',
}],
attrs: {
placeholder: '请选择集群类型',
},
},
{
key: 'kafkaVersion',
label: 'kafka版本',
invisible: item ? false : true,
rules: [{
required: false,
message: '请输入kafka版本',
}],
attrs: {
placeholder: '请输入kafka版本',
disabled: true,
},
},
{
key: 'securityProperties',
label: '安全协议',
type: 'text_area',
rules: [{
required: false,
message: '请输入安全协议',
}],
attrs: {
placeholder: '请输入安全协议',
rows: 6,
},
},
],
formData: item ? item : {},
visible: true,
width: 590,
title: '注册集群',
onSubmit: (value: IRegister) => {
value.idc = region.currentRegion;
if (item) {
value.clusterId = item.clusterId;
registerCluster(value).then(data => {
admin.getMetaData(true);
notification.success({ message: '修改集群成功' });
});
} else {
createCluster(value).then(data => {
admin.getMetaData(true);
notification.success({ message: '注册集群成功' });
});
}
},
};
wrapper.open(this.xFormModal);
}
public pauseMonitor(item: IMetaData) {
const info = item.status === 1 ? '暂停监控' : '开始监控';
const status = item.status === 1 ? 0 : 1;
pauseMonitoring(item.clusterId, status).then(data => {
admin.getMetaData(true);
notification.success({ message: `${info}成功` });
});
}
public showMonitor = (record: IMetaData) => {
admin.getBrokersRegions(record.clusterId).then((data) => {
confirm({
// tslint:disable-next-line:jsx-wrap-multiline
title: <>
<span className="offline_span">
&nbsp;
<a>
<Tooltip placement="right" title={'当前集群存在逻辑集群,无法申请下线'} >
<Icon type="question-circle" />
</Tooltip>
</a>
</span>
</>,
icon: 'none',
content: this.deleteMonitorModal(data),
width: 500,
okText: '确认',
cancelText: '取消',
onOk() {
if (data.length) {
return message.warning('存在逻辑集群,无法申请下线!');
}
admin.deleteCluster(record.clusterId).then(data => {
notification.success({ message: '删除成功' });
});
},
});
});
}
public deleteMonitorModal = (source: any) => {
const cellStyle = {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
};
const monitorColumns = [
{
title: '集群名称',
dataIndex: 'name',
key: 'name',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
render: (t: string) => {
return (
<Tooltip placement="bottomLeft" title={t} >{t}</Tooltip>
);
},
},
];
return (
<>
<div className="render_offline">
<Table
rowKey="key"
dataSource={source}
columns={monitorColumns}
scroll={{ x: 300, y: 200 }}
pagination={false}
bordered={true}
/>
</div>
</>
);
}
public getData<T extends IMetaData>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IMetaData) =>
(item.clusterName !== undefined && item.clusterName !== null) && item.clusterName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public getColumns = () => {
const cols = getAdminClusterColumns();
const col = {
title: '操作',
render: (value: string, item: IMetaData) => (
<>
<a
onClick={this.createOrRegisterCluster.bind(this, item)}
className="action-button"
>
</a>
<Popconfirm
title={`确定${item.status === 1 ? '暂停' : '开始'}${item.clusterName}监控?`}
onConfirm={() => this.pauseMonitor(item)}
>
<a
className="action-button"
>
{item.status === 1 ? '暂停监控' : '开始监控'}
</a>
</Popconfirm>
<a onClick={this.showMonitor.bind(this, item)}>
</a>
</>
),
};
cols.push(col as any);
return cols;
}
public renderClusterList() {
return (
<>
<div className="container">
<div className="table-operation-panel">
<ul>
{this.renderSearch('', '请输入集群名称')}
<li className="right-btn-1">
<Button type="primary" onClick={this.createOrRegisterCluster.bind(this, null)}></Button>
</li>
</ul>
</div>
<div className="table-wrapper">
<Table
rowKey="key"
loading={admin.loading}
dataSource={this.getData(admin.metaList)}
columns={this.getColumns()}
pagination={customPagination}
/>
</div>
</div>
</>
);
}
public componentDidMount() {
admin.getMetaData(true);
cluster.getClusterModes();
admin.getDataCenter();
}
public render() {
return (
admin.metaList ? <> {this.renderClusterList()} </> : null
);
}
}

View File

@@ -0,0 +1,319 @@
import * as React from 'react';
import { IUser, IUploadFile, IConfigure, IMetaData, IBrokersPartitions } from 'types/base-type';
import { users } from 'store/users';
import { version } from 'store/version';
import { showApplyModal, showModifyModal, showConfigureModal } from 'container/modal/admin';
import { Popconfirm, Tooltip } from 'component/antd';
import { admin } from 'store/admin';
import { cellStyle } from 'constants/table';
import { timeFormat } from 'constants/strategy';
import { urlPrefix } from 'constants/left-menu';
import moment = require('moment');
export const getUserColumns = () => {
const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: '35%',
},
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: '30%',
render: (text: string, record: IUser) => {
return (
<span className="table-operation">
<a onClick={() => showApplyModal(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => users.deleteUser(record.username)}
>
<a></a>
</Popconfirm>
</span>);
},
},
];
return columns;
};
export const getVersionColumns = () => {
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '文件名称',
dataIndex: 'fileName',
key: 'fileName',
render: (text: string, record: IUploadFile) => {
return (
<Tooltip placement="topLeft" title={text} >
<a href={`${urlPrefix}/info?fileId=${record.id}`} target="_blank">{text}</a>
</Tooltip>);
},
}, {
title: 'MD5',
dataIndex: 'fileMd5',
key: 'fileMd5',
onCell: () => ({
style: {
maxWidth: 120,
...cellStyle,
},
}),
render: (text: string) => {
return (
<Tooltip placement="bottomLeft" title={text} >
{text.substring(0, 8)}
</Tooltip>);
},
}, {
title: '更新时间',
dataIndex: 'gmtModify',
key: 'gmtModify',
render: (t: number) => moment(t).format(timeFormat),
}, {
title: '更新人',
dataIndex: 'operator',
key: 'operator',
}, {
title: '备注',
dataIndex: 'description',
key: 'description',
onCell: () => ({
style: {
maxWidth: 200,
...cellStyle,
},
}),
render: (text: string) => {
return (
<Tooltip placement="topLeft" title={text} >
{text}
</Tooltip>);
},
}, {
title: '操作',
dataIndex: 'operation',
key: 'operation',
render: (text: string, record: IUploadFile) => {
return (
<span className="table-operation">
<a onClick={() => showModifyModal(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => version.deleteFile(record.id)}
>
<a></a>
</Popconfirm>
</span>);
},
},
];
return columns;
};
export const getConfigureColumns = () => {
const columns = [
{
title: '配置键',
dataIndex: 'configKey',
key: 'configKey',
width: '20%',
sorter: (a: IConfigure, b: IConfigure) => a.configKey.charCodeAt(0) - b.configKey.charCodeAt(0),
},
{
title: '配置值',
dataIndex: 'configValue',
key: 'configValue',
width: '30%',
sorter: (a: IConfigure, b: IConfigure) => a.configValue.charCodeAt(0) - b.configValue.charCodeAt(0),
render: (t: string) => {
return t.substr(0, 1) === '{' && t.substr(0, -1) === '}' ? JSON.stringify(JSON.parse(t), null, 4) : t;
},
},
{
title: '修改时间',
dataIndex: 'gmtModify',
key: 'gmtModify',
width: '20%',
sorter: (a: IConfigure, b: IConfigure) => b.gmtModify - a.gmtModify,
render: (t: number) => moment(t).format(timeFormat),
},
{
title: '描述信息',
dataIndex: 'configDescription',
key: 'configDescription',
width: '20%',
onCell: () => ({
style: {
maxWidth: 180,
...cellStyle,
},
}),
},
{
title: '操作',
width: '10%',
render: (text: string, record: IConfigure) => {
return (
<span className="table-operation">
<a onClick={() => showConfigureModal(record)}></a>
<Popconfirm
title="确定删除?"
onConfirm={() => admin.deleteConfigure(record.configKey)}
>
<a></a>
</Popconfirm>
</span>);
},
},
];
return columns;
};
const renderClusterHref = (value: number | string, item: IMetaData, key: number) => {
return ( // 0 暂停监控--不可点击 1 监控中---可正常点击
<>
{item.status === 1 ? <a href={`${urlPrefix}/admin/cluster-detail?clusterId=${item.clusterId}#${key}`}>{value}</a>
: <a style={{ cursor: 'not-allowed', color: '#999' }}>{value}</a>}
</>
);
};
export const getAdminClusterColumns = () => {
return [
{
title: '集群ID',
dataIndex: 'clusterId',
key: 'clusterId',
sorter: (a: IMetaData, b: IMetaData) => b.clusterId - a.clusterId,
},
{
title: '集群名称',
dataIndex: 'clusterName',
key: 'clusterName',
sorter: (a: IMetaData, b: IMetaData) => a.clusterName.charCodeAt(0) - b.clusterName.charCodeAt(0),
render: (text: string, item: IMetaData) => renderClusterHref(text, item, 1),
},
{
title: 'Topic数',
dataIndex: 'topicNum',
key: 'topicNum',
sorter: (a: any, b: IMetaData) => b.topicNum - a.topicNum,
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 2),
},
{
title: 'Broker数',
dataIndex: 'brokerNum',
key: 'brokerNum',
sorter: (a: IMetaData, b: IMetaData) => b.brokerNum - a.brokerNum,
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 3),
},
{
title: 'Consumer数',
dataIndex: 'consumerGroupNum',
key: 'consumerGroupNum',
sorter: (a: IMetaData, b: IMetaData) => b.consumerGroupNum - a.consumerGroupNum,
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 4),
},
{
title: 'Region数',
dataIndex: 'regionNum',
key: 'regionNum',
sorter: (a: IMetaData, b: IMetaData) => b.regionNum - a.regionNum,
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 5),
},
{
title: 'Controllerld',
dataIndex: 'controllerId',
key: 'controllerId',
sorter: (a: IMetaData, b: IMetaData) => b.controllerId - a.controllerId,
render: (text: number, item: IMetaData) => renderClusterHref(text, item, 7),
},
{
title: '监控中',
dataIndex: 'status',
key: 'status',
sorter: (a: IMetaData, b: IMetaData) => b.key - a.key,
render: (value: number) => value === 1 ?
<span className="success"></span > : <span className="fail"></span>,
},
];
};
export const getPartitionInfoColumns = () => {
return [{
title: 'Topic',
dataIndex: 'topicName',
key: 'topicName',
width: '21%',
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
}, {
title: 'Leader',
dataIndex: 'leaderPartitionList',
width: '20%',
onCell: () => ({
style: {
maxWidth: 250,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (value: number[]) => {
return (
<Tooltip placement="bottomLeft" title={value.join('、')}>
{value.map(i => <span key={i} className="p-params">{i}</span>)}
</Tooltip>
);
},
}, {
title: '副本',
dataIndex: 'followerPartitionIdList',
width: '22%',
onCell: () => ({
style: {
maxWidth: 250,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (value: number[]) => {
return (
<Tooltip placement="bottomLeft" title={value.join('、')}>
{value.map(i => <span key={i} className="p-params">{i}</span>)}
</Tooltip>
);
},
}, {
title: '未同步副本',
dataIndex: 'notUnderReplicatedPartitionIdList',
width: '22%',
onCell: () => ({
style: {
maxWidth: 250,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (value: number[]) => {
return (
<Tooltip placement="bottomLeft" title={value.join('、')}>
{value.map(i => <span key={i} className="p-params p-params-unFinished">{i}</span>)}
</Tooltip>
);
},
}];
};

View File

@@ -0,0 +1,74 @@
import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { Table, Button, Spin } from 'component/antd';
import { admin } from 'store/admin';
import { observer } from 'mobx-react';
import { IConfigure } from 'types/base-type';
import { users } from 'store/users';
import { pagination } from 'constants/table';
import { getConfigureColumns } from './config';
import { showConfigureModal } from 'container/modal/admin';
@observer
export class ConfigureManagement extends SearchAndFilterContainer {
public state = {
searchKey: '',
filterRole: '',
};
public componentDidMount() {
admin.getConfigure();
}
public getData<T extends IConfigure>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IConfigure) =>
((item.configKey !== undefined && item.configKey !== null) && item.configKey.toLowerCase().includes(searchKey as string))
|| ((item.configValue !== undefined && item.configValue !== null) && item.configValue.toLowerCase().includes(searchKey as string))
|| ((item.configDescription !== undefined && item.configDescription !== null) &&
item.configDescription.toLowerCase().includes(searchKey as string)),
) : origin ;
return data;
}
public renderTable() {
return (
<Spin spinning={users.loading}>
<Table
rowKey="key"
columns={getConfigureColumns()}
dataSource={this.getData(admin.configureList)}
pagination={pagination}
/>
</Spin>
);
}
public renderOperationPanel() {
return (
<ul>
{this.renderSearch('', '请输入配置键、值或描述')}
<li className="right-btn-1">
<Button type="primary" onClick={() => showConfigureModal()}></Button>
</li>
</ul>
);
}
public render() {
return (
<div className="container">
<div className="table-operation-panel">
{this.renderOperationPanel()}
</div>
<div className="table-wrapper">
{this.renderTable()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,210 @@
import { EChartOption } from 'echarts/lib/echarts';
import moment from 'moment';
import { ICurve } from 'container/common-curve/config';
import { adminMonitor } from 'store/admin-monitor';
import { parseBrokerMetricOption } from './parser';
export interface IPeriod {
label: string;
key: string;
dateRange: [moment.Moment, moment.Moment];
}
export const getMoment = () => {
return moment();
};
export const baseColors = ['#F28E61', '#7082A6', '#5AD2A2', '#E96A72', '#59AEE9', '#65A8BF', '#9D7ECF'];
export enum curveKeys {
'byteIn/byteOut' = 'byteIn/byteOut',
bytesRejectedPerSec = 'bytesRejectedPerSec',
failFetchRequestPerSec = 'failFetchRequestPerSec',
failProduceRequestPerSec = 'failProduceRequestPerSec',
fetchConsumerRequestPerSec = 'fetchConsumerRequestPerSec',
healthScore = 'healthScore',
messagesInPerSec = 'messagesInPerSec',
networkProcessorIdlPercent = 'networkProcessorIdlPercent',
produceRequestPerSec = 'produceRequestPerSec',
requestHandlerIdlPercent = 'requestHandlerIdlPercent',
requestQueueSize = 'requestQueueSize',
responseQueueSize = 'responseQueueSize',
totalTimeFetchConsumer99Th = 'totalTimeFetchConsumer99Th',
totalTimeProduce99Th = 'totalTimeProduce99Th',
}
export const byteCurves: ICurve[] = [
{
title: 'byteIn/byteOut',
path: curveKeys['byteIn/byteOut'],
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'bytesRejectedPerSec',
path: curveKeys.bytesRejectedPerSec,
api: adminMonitor.getBrokersChartsData,
colors: ['#E96A72'],
},
];
export const perSecCurves: ICurve[] = [
{
title: 'failFetchRequestPerSec',
path: curveKeys.failFetchRequestPerSec,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'failProduceRequestPerSec',
path: curveKeys.failProduceRequestPerSec,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'fetchConsumerRequestPerSec',
path: curveKeys.fetchConsumerRequestPerSec,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'produceRequestPerSec',
path: curveKeys.produceRequestPerSec,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
},
];
export const otherCurves: ICurve[] = [
{
title: 'healthScore',
path: curveKeys.healthScore,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'messagesInPerSec',
path: curveKeys.messagesInPerSec,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'networkProcessorIdlPercent',
path: curveKeys.networkProcessorIdlPercent,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'requestHandlerIdlPercent',
path: curveKeys.requestHandlerIdlPercent,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'requestQueueSize',
path: curveKeys.requestQueueSize,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'responseQueueSize',
path: curveKeys.responseQueueSize,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'totalTimeFetchConsumer99Th',
path: curveKeys.totalTimeFetchConsumer99Th,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
}, {
title: 'totalTimeProduce99Th',
path: curveKeys.totalTimeProduce99Th,
api: adminMonitor.getBrokersChartsData,
colors: baseColors,
},
];
export enum curveType {
byteCurves = 'byteCurves',
perSecCurves = 'perSecCurves',
other = 'other',
}
export interface ICurveType {
type: curveType;
title: string;
curves: ICurve[];
parser: (option: ICurve, data: any[]) => EChartOption;
}
export const byteTypeCurves: ICurveType[] = [
{
type: curveType.byteCurves,
title: 'byte',
curves: byteCurves.concat(perSecCurves, otherCurves),
parser: parseBrokerMetricOption,
},
];
export const perSecTypeCurves: ICurveType[] = [
{
type: curveType.perSecCurves,
title: 'perSec',
curves: perSecCurves,
parser: parseBrokerMetricOption,
},
];
export const otherTypeCurves: ICurveType[] = [
{
type: curveType.other,
title: 'other',
curves: otherCurves,
parser: parseBrokerMetricOption,
},
];
export const allCurves: ICurveType[] = [].concat(byteTypeCurves);
const curveKeyMap = new Map<string, {typeInfo: ICurveType, curveInfo: ICurve}>();
allCurves.forEach(t => {
t.curves.forEach(c => {
curveKeyMap.set(c.path, {
typeInfo: t,
curveInfo: c,
});
});
});
export const CURVE_KEY_MAP = curveKeyMap;
export const PERIOD_RADIO = [
{
label: '10分钟',
key: 'tenMin',
get dateRange() {
return [getMoment().subtract(10, 'minute'), getMoment()];
},
},
{
label: '1小时',
key: 'oneHour',
get dateRange() {
return [getMoment().subtract(1, 'hour'), getMoment()];
},
},
{
label: '6小时',
key: 'sixHour',
get dateRange() {
return [getMoment().subtract(6, 'hour'), getMoment()];
},
},
{
label: '近1天',
key: 'oneDay',
get dateRange() {
return [getMoment().subtract(1, 'day'), getMoment()];
},
},
{
label: '近1周',
key: 'oneWeek',
get dateRange() {
return [getMoment().subtract(7, 'day'), getMoment()];
},
},
] as IPeriod[];
const periodRadioMap = new Map<string, IPeriod>();
PERIOD_RADIO.forEach(p => {
periodRadioMap.set(p.key, p);
});
export const PERIOD_RADIO_MAP = periodRadioMap;

View File

@@ -0,0 +1,14 @@
.curve-wrapper {
background-color: #fff;
padding: 24px;
position: relative;
}
.right-btn {
position: absolute;
right: 24px;
top: 24px;
}
.operator-select {
position: absolute;
top: 34px;
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import './index.less';
import { Radio, DatePicker, RadioChangeEvent, RangePickerValue, Button, Icon } from 'component/antd';
import { curveInfo } from 'store/curve-info';
import { curveKeys, CURVE_KEY_MAP, PERIOD_RADIO_MAP, PERIOD_RADIO } from './config';
import moment = require('moment');
import { observer } from 'mobx-react';
import { timeStampStr } from 'constants/strategy';
@observer
export class DataCurveFilter extends React.Component {
public handleRangeChange = (dates: RangePickerValue, dateStrings: [string, string]) => {
curveInfo.setTimeRange(dates as [moment.Moment, moment.Moment]);
this.refreshAll();
}
public radioChange = (e: RadioChangeEvent) => {
const { value } = e.target;
curveInfo.setTimeRange(PERIOD_RADIO_MAP.get(value).dateRange);
this.refreshAll();
}
public refreshAll = () => {
Object.keys(curveKeys).forEach((c: curveKeys) => {
const { typeInfo, curveInfo: option } = CURVE_KEY_MAP.get(c);
const { parser } = typeInfo;
curveInfo.getCommonCurveData(option, parser, true);
});
}
public render() {
return (
<>
<Radio.Group onChange={this.radioChange} defaultValue={curveInfo.periodKey}>
{PERIOD_RADIO.map(p => <Radio.Button key={p.key} value={p.key}>{p.label}</Radio.Button>)}
</Radio.Group>
<DatePicker.RangePicker
format={timeStampStr}
onChange={this.handleRangeChange}
className="ml-10"
value={curveInfo.timeRange}
/>
<div className="right-btn">
<Button onClick={this.refreshAll}><Icon className="dsui-icon-shuaxin1 mr-4" type="reload" /></Button>
</div>
</>
);
}
}

View File

@@ -0,0 +1,147 @@
import moment from 'moment';
import { EChartOption } from 'echarts';
import { ICurve, ILineData, baseLineLegend, baseLineGrid, baseAxisStyle, noAxis, UNIT_HEIGHT } from 'container/common-curve/config';
import { IClusterMetrics, ISeriesOption } from 'types/base-type';
import { timeFormat } from 'constants/strategy';
import { getFilterSeries } from 'lib/chart-utils';
import { dealFlowData } from 'lib/chart-utils';
export const getBaseOptions = (option: ICurve, data: ILineData[]) => {
const date = (data || []).map(i => moment(i.timeStamp).format(timeFormat));
return {
animationDuration: 200,
tooltip: {
trigger: 'axis',
},
toolbox: {
feature: {
saveAsImage: {},
},
},
legend: {
...baseLineLegend,
bottom: '0',
align: 'auto',
},
grid: {
...baseLineGrid,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: date,
...baseAxisStyle,
},
yAxis: {
type: 'value',
...baseAxisStyle,
...noAxis,
name: option.unit || '',
nameTextStyle: {
lineHeight: UNIT_HEIGHT,
},
},
series: [{
type: 'line',
data: data.map(i => {
return Number(i.value);
}),
}],
} as EChartOption;
};
export const parseLine = (option: ICurve, data: ILineData[]): EChartOption => {
return Object.assign({}, getBaseOptions(option, data), {
legend: {
...baseLineLegend,
bottom: '0',
align: 'auto',
},
}) as EChartOption;
};
export const parseBrokerMetricOption = (option: ICurve, data: IClusterMetrics[]): EChartOption => {
let name;
let series: ISeriesOption[];
data = data || [];
data = data.map(item => {
return {
time: moment(item.gmtCreate).format(timeFormat),
...item,
};
});
data = data.sort((a, b) => a.gmtCreate - b.gmtCreate);
const legend = option.path === 'byteIn/byteOut' ? ['bytesInPerSec', 'bytesOutPerSec'] : [option.path];
series = Array.from(legend, (item: string) => ({
name: item,
id: item,
type: 'line',
symbol: 'circle',
showSymbol: false,
smooth: true,
encode: {
x: 'time',
y: item,
tooltip: [
item,
],
},
data: data.map(row => row[item] !== null ? Number(row[item]) : null),
}));
const filterSeries = getFilterSeries(series);
const { name: unitName, data: xData } = dealFlowData(legend, data);
name = unitName;
data = xData;
return {
animationDuration: 200,
tooltip: {
trigger: 'axis',
},
toolbox: {
feature: {
saveAsImage: {},
},
},
grid: {
...baseLineGrid,
},
xAxis: {
splitLine: null,
type: 'time',
},
yAxis: {
type: 'value',
...baseAxisStyle,
...noAxis,
name,
nameTextStyle: {
lineHeight: UNIT_HEIGHT,
},
},
legend: {
data: legend,
...baseLineLegend,
bottom: '0',
align: 'auto',
},
dataset: {
source: data,
},
series: filterSeries,
};
};
export function isM(arr: number[]) {
const filterData = arr.filter(i => i !== 0);
if (filterData.length) return filterData.reduce((cur, pre) => cur + pre) / filterData.length >= 1000000;
return false;
}
export function isK(arr: number[]) {
const filterData = arr.filter(i => i !== 0);
if (filterData.length) return filterData.reduce((cur, pre) => cur + pre) / filterData.length >= 1000;
return false;
}

View File

@@ -0,0 +1,13 @@
export * from './cluster-list';
export * from './cluster-detail';
export * from './broker-detail';
export * from './user-management';
export * from './version-management';
export * from './operation-management';
export * from './operation-detail';
export * from './bill-management';
export * from './admin-app-list';
export * from './operation-management/migration-detail';
export * from './configure-management';
export * from './individual-bill';
export * from './bill-detail';

View File

@@ -0,0 +1,159 @@
import * as React from 'react';
import { Table, Tabs, DatePicker, notification, Icon } from 'component/antd';
import { pagination } from 'constants/table';
import moment, { Moment } from 'moment';
import { BarChartComponet } from 'component/chart';
import { observer } from 'mobx-react';
import { getBillListColumns } from '../user-center/config';
import { urlPrefix } from 'constants/left-menu';
import { timeMonthStr } from 'constants/strategy';
import { users } from 'store/users';
import { admin } from 'store/admin';
import * as XLSX from 'xlsx';
const { TabPane } = Tabs;
const { RangePicker } = DatePicker;
@observer
export class IndividualBill extends React.Component {
public username: string;
public state = {
mode: ['month', 'month'] as any,
value: [moment(new Date()).subtract(6, 'months'), moment()] as any,
};
private startTime: number = moment(new Date()).subtract(6, 'months').valueOf();
private endTime: number = moment().valueOf();
private chart: any = null;
public getData() {
const { startTime, endTime } = this;
return admin.getBillStaffList(this.username, startTime, endTime);
}
public handleDownLoad() {
const tableData = admin.billStaff.map(item => {
return {
// tslint:disable
'月份': item.gmtMonth,
'Topic数量': item.topicNum,
'quota数量': item.quota,
'金额': item.cost,
};
});
const data = [].concat(tableData);
const wb = XLSX.utils.book_new();
// json转sheet
const ws = XLSX.utils.json_to_sheet(data, {
header: ['月份', 'Topic数量', 'quota数量', '金额'],
});
// XLSX.utils.
XLSX.utils.book_append_sheet(wb, ws, 'bill');
// 输出
XLSX.writeFile(wb, 'bill' + '.xlsx');
}
public disabledDateTime = (current: Moment) => {
return current && current > moment().endOf('day');
}
public handleChartSearch = (date: moment.Moment[]) => {
this.setState({
value: date,
mode: ['month', 'month'] as any,
});
this.startTime = date[0].valueOf();
this.endTime = date[1].valueOf();
if (this.startTime >= this.endTime) {
return notification.error({ message: '开始时间不能大于或等于结束时间' });
}
this.getData();
this.handleRefreshChart();
}
public handleRefreshChart = () => {
this.chart.handleRefreshChart();
}
public renderTableList() {
const adminUrl=`${urlPrefix}/admin/bill-detail`
return (
<Table
rowKey="key"
columns={getBillListColumns(adminUrl)}
dataSource={admin.billStaff}
pagination={pagination}
/>
);
}
public renderChart() {
return (
<div className="chart-box">
<BarChartComponet ref={(ref) => this.chart = ref } getChartData={this.getData.bind(this, null)} />
</div>
);
}
public renderDatePick() {
const { value, mode } = this.state;
return (
<>
<div className="op-panel">
<span>
<RangePicker
ranges={{
: [moment(new Date()).subtract(6, 'months'), moment()],
: [moment().startOf('year'), moment().endOf('year')],
}}
defaultValue={[moment(new Date()).subtract(6, 'months'), moment()]}
value={value}
mode={mode}
format={timeMonthStr}
onChange={this.handleChartSearch}
onPanelChange={this.handleChartSearch}
/>
</span>
<span>
<Icon type="download" onClick={this.handleDownLoad.bind(this, null)} />
</span>
</div>
</>
);
}
public render() {
this.username = users.currentUser.username;
return (
<>
<div className="container">
<Tabs defaultActiveKey="1" type="card">
<TabPane
tab={<>
<span></span>&nbsp;
<a
// tslint:disable-next-line:max-line-length
href="https://github.com/didi/kafka-manager"
target="_blank"
>
<Icon type="question-circle" />
</a>
</>}
key="1"
>
{this.renderDatePick()}
{this.username ? this.renderChart() : null}
</TabPane>
</Tabs>
<div className="table-content">
{this.renderTableList()}
</div>
</div>
</>
);
}
}

View File

@@ -0,0 +1,82 @@
import * as React from 'react';
import { ILabelValue, ITasksMetaData } from 'types/base-type';
import { Descriptions } from 'antd';
import { observer } from 'mobx-react';
import { timeFormat } from 'constants/strategy';
import { urlPrefix } from 'constants/left-menu';
import { Table } from 'component/antd';
import { pagination } from 'constants/table';
import moment from 'moment';
interface IEassProps {
tasksMetaData?: ITasksMetaData;
}
@observer
export class EassentialInfo extends React.Component<IEassProps> {
public render() {
const { tasksMetaData } = this.props;
let tasks = {} as ITasksMetaData;
tasks = tasksMetaData ? tasksMetaData : tasks;
const gmtCreate = moment(tasks.gmtCreate).format(timeFormat);
const options = [{
value: tasks.taskId,
label: '任务ID',
}, {
value: tasks.clusterId,
label: '集群ID',
}, {
value: tasks.clusterName,
label: '集群名称',
}, {
value: gmtCreate,
label: '创建时间',
}, {
value: tasks.kafkaPackageName,
label: 'kafka包',
}, {
value: tasks.kafkaPackageMd5,
label: 'kafka包 MD5',
}, {
value: tasks.operator,
label: '操作人',
}];
const optionsHost = [{
value: tasks.hostList,
label: '升级主机列表',
}, {
value: tasks.pauseHostList,
label: '升级的主机暂停点',
}];
return(
<>
<Descriptions column={3}>
{options.map((item: ILabelValue, index) => (
<Descriptions.Item key={item.label || index} label={item.label}>{item.value}</Descriptions.Item>
))}
<Descriptions.Item key="server" label="server配置名">
<a href={`${urlPrefix}/info?fileId=${tasks.serverPropertiesFileId || ''}`} target="_blank">{tasks.serverPropertiesName}</a>
</Descriptions.Item>
<Descriptions.Item key="server" label="server配置 MD5">{tasks.serverPropertiesMd5}</Descriptions.Item>
</Descriptions>
<Descriptions column={1}>
{optionsHost.map((item: any, index) => (
<Descriptions.Item key={item.label || index} label="">
<Table
columns={[{
title: item.label,
dataIndex: '',
key: '',
}]}
dataSource={item.value}
pagination={pagination}
rowKey="key"
/>
</Descriptions.Item>
))}
</Descriptions>
</>
);
}
}

View File

@@ -0,0 +1,26 @@
.task-status li{
margin-left: -10px;
font-size: 12px;
font-weight: 100;
}
.complete{
color: #76a8ca;
}
.executing {
color: #66c84c;
}
.pending {
color: #de9845;
}
.modal-body{
max-height: 400px;
overflow-y: auto;
}
.btn-position{
margin-right: 10px;
}

View File

@@ -0,0 +1,128 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Tabs, PageHeader, Button, notification, Popconfirm, Spin } from 'antd';
import { handleTabKey } from 'lib/utils';
import { EassentialInfo } from './essential-info';
import { TaskStatusDetails } from './taskStatus-details';
import { ITasksMetaData, ITrigger } from 'types/base-type';
import { triggerClusterTask } from 'lib/api';
import { handlePageBack } from 'lib/utils';
import { admin } from 'store/admin';
import Url from 'lib/url-parser';
import './index.less';
const { TabPane } = Tabs;
let showStatus: boolean = false;
let showContinue: boolean = false;
@observer
export class OperationDetail extends React.Component {
public taskId: number;
public taskName: string;
public state = {
showContinue: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.taskId = Number(url.search.taskId);
}
public bindClick() {
const type = showContinue ? 'start' : 'pause';
const params = {
taskId: this.taskId,
action: type,
hostname: '',
} as ITrigger;
triggerClusterTask(params).then(data => {
admin.getSubtasksStatus(this.taskId);
notification.success({ message: `${showContinue ? '继续部署' : '暂停'}成功!` });
});
}
public callBackOrCancel(type: string) {
const params = {
taskId: this.taskId,
action: type,
hostname: '',
} as ITrigger;
triggerClusterTask(params).then(data => {
admin.getSubtasksStatus(this.taskId);
notification.success({ message: `${type === 'rollback' ? '回滚任务' : '取消'}成功` });
});
}
public handleVal(value: number) {
showStatus = (value + '').includes('100') ? true : false;
}
public componentDidMount() {
admin.getTasksMetadata(this.taskId);
admin.getSubtasksStatus(this.taskId);
}
public render() {
// 任务状态: 30:运行中(展示暂停), 40:暂停(展示开始), 100:完成(都置灰)
showStatus = admin.taskStatusDetails && admin.taskStatusDetails.status === 100 ? true : false;
showContinue = admin.taskStatusDetails && admin.taskStatusDetails.status === 40 ? true : false;
const showRollBack = admin.taskStatusDetails && admin.taskStatusDetails.rollback;
let tasks = {} as ITasksMetaData;
tasks = admin.tasksMetaData ? admin.tasksMetaData : tasks;
return (
<>
<Spin spinning={admin.loading}>
<PageHeader
className="detail hotspot-header"
onBack={() => handlePageBack('/admin/operation#1')}
title={`任务名称${tasks.taskName ? '/' + tasks.taskName : ''}`}
extra={[
<Button key="1" type="primary" disabled={showStatus} >
<Popconfirm
title={`确定${showContinue ? '开始' : '暂停'}`}
onConfirm={() => this.bindClick()}
>
<a>{showContinue ? '开始' : '暂停'}</a>
</Popconfirm>
</Button>,
<Button
key="2"
type="primary"
disabled={showRollBack || showStatus}
>
<Popconfirm
title={`确定回滚?`}
onConfirm={() => this.callBackOrCancel('rollback')}
>
<a></a>
</Popconfirm>
</Button>,
<Button
key="3"
type="primary"
disabled={showStatus}
>
<Popconfirm
title={`确定回滚?`}
onConfirm={() => this.callBackOrCancel('cancel')}
>
<a></a>
</Popconfirm>
</Button>,
]}
>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="基本信息" key="1">
<EassentialInfo tasksMetaData={tasks} />
</TabPane>
<TabPane tab="任务状态详情" key="2">
<TaskStatusDetails handleVal={(value: number) => this.handleVal(value)} />
</TabPane>
</Tabs>
</PageHeader>
</Spin>
</>
);
}
}

View File

@@ -0,0 +1,211 @@
import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { pagination } from 'constants/table';
import { admin } from 'store/admin';
import { Table, Popconfirm, notification } from 'component/antd';
import { IEnumsMap, ITaskStatusDetails, ISubtasksStatus, ITrigger, IXFormWrapper } from 'types/base-type';
import { tableFilter } from 'lib/utils';
import { triggerClusterTask } from 'lib/api';
import { wrapper } from 'store';
import { observer } from 'mobx-react';
import Url from 'lib/url-parser';
import './index.less';
let taskStatus = [] as IEnumsMap[];
let task = {} as ITaskStatusDetails;
let subTaskStatusList = [] as ISubtasksStatus[];
let statusNum: number;
@observer
export class TaskStatusDetails extends SearchAndFilterContainer {
public taskId: number;
public timer = null as any;
public state = {
searchKey: '',
filterGroupVisible: false,
filterStatusVisible: false,
};
private xFormWrapper: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.taskId = Number(url.search.taskId);
}
public renderDataMigrationTasks(subTaskStatusList: ISubtasksStatus[]) {
const groupId = Object.assign({
title: '分组ID',
dataIndex: 'groupId',
key: 'groupId',
width: '20%',
filters: tableFilter<any>(subTaskStatusList, 'groupId'),
onFilter: (value: number, record: ISubtasksStatus) => record.groupId === value,
}, this.renderColumnsFilter('filterGroupVisible'));
const status = Object.assign({
title: '状态',
dataIndex: 'status',
key: 'status',
width: '20%',
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
onFilter: (value: number, record: ISubtasksStatus) => record.status === +value,
render: (t: number) => {
let messgae: string;
taskStatus.map(ele => {
if (ele.code === t) {
messgae = ele.message;
}
});
return(
<span className={t === 102 ? 'fail' : t === 101 ? 'succee' : ''}>{messgae}</span>
);
},
}, this.renderColumnsFilter('filterStatusVisible'));
const columns = [{
title: '主机名',
dataIndex: 'hostname',
key: 'hostname',
width: '20%',
sorter: (a: ISubtasksStatus, b: ISubtasksStatus) => a.hostname.charCodeAt(0) - b.hostname.charCodeAt(0),
}, {
title: '机器角色',
dataIndex: 'kafkaRoles',
key: 'kafkaRoles',
width: '20%',
},
groupId,
status,
{
title: '操作',
width: '20%',
render: (value: any, record: ISubtasksStatus) => {
return (
<>
<span className="btn-position">
<Popconfirm
title={`确定忽略?`}
onConfirm={() => this.bindClick(record, 'ignore')}
>
<a></a>
</Popconfirm>
</span>
<a onClick={() => this.handleViewLog(record)}></a>
</>
);
},
},
];
return (
<>
<Table
columns={columns}
dataSource={subTaskStatusList}
pagination={pagination}
rowKey="hostname"
/>
</>
);
}
public handleViewLog = async (record: ISubtasksStatus) => {
await admin.getClusterTaskLog(this.taskId, record.hostname);
this.xFormWrapper = {
type: 'drawer',
visible: true,
width: 600,
title: '查看日志',
customRenderElement: this.showLog(),
nofooter: true,
noform: true,
onSubmit: (value: any) => {
// TODO:
},
};
await wrapper.open(this.xFormWrapper);
}
public showLog() {
return (
<>
<div className="config-info">
{admin.clusterTaskLog}
</div>
</>
);
}
public bindClick(record: ISubtasksStatus, type: string) {
const params = {
taskId: this.taskId,
action: type,
hostname: record.hostname,
} as ITrigger;
triggerClusterTask(params).then(data => {
admin.getSubtasksStatus(this.taskId);
notification.success({ message: `${type === 'cancel' ? '取消' : '忽略'}成功!` });
});
}
public iTimer = () => {
this.timer = setInterval(() => {
admin.getSubtasksStatus(this.taskId);
}, 3 * 1 * 1000);
}
public componentDidMount() {
setTimeout(this.iTimer, 0);
admin.getConfigsTaskStatus();
}
public componentWillUnmount() {
clearInterval(this.timer);
}
public getData<T extends ISubtasksStatus>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: ISubtasksStatus) =>
(item.hostname !== undefined && item.hostname !== null) && item.hostname.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public render() {
let message = '';
if (admin.taskStatusDetails) {
task = admin.taskStatusDetails;
subTaskStatusList = admin.taskStatusDetails.subTaskStatusList;
statusNum = admin.taskStatusDetails.status;
this.props.handleVal(statusNum);
}
taskStatus = admin.configsTaskStatus ? admin.configsTaskStatus : [] as IEnumsMap[];
taskStatus.forEach(ele => {
if (ele.code === task.status) {
message = ele.message;
}
});
return(
<>
<div className="k-row" >
<ul className="k-tab task-status">
<li>
<span className="complete">{message}</span>
<span className="complete">{task.sumCount}</span>
<span className="success">{task.successCount}</span>
<span className="fail">{task.failedCount}</span>
<span className="executing">{task.runningCount}</span>
<span className="pending">{task.waitingCount}</span>
</li>
{this.renderSearch('', '请输入主机名')}
</ul>
{this.renderDataMigrationTasks(this.getData(subTaskStatusList))}
</div>
</>
);
}
}

View File

@@ -0,0 +1,172 @@
import * as React from 'react';
import { Table, Tabs, Button } from 'component/antd';
import { observer } from 'mobx-react';
import { ITaskManage, IEnumsMap, ITasksEnums } from 'types/base-type';
import { SearchAndFilterContainer } from 'container/search-filter';
import { tableFilter } from 'lib/utils';
import { pagination } from 'constants/table';
import { addMigrationTask } from 'container/modal';
import { admin } from 'store/admin';
import moment from 'moment';
import './index.less';
import { timeFormat } from 'constants/strategy';
import { region } from 'store';
@observer
export class ClusterTask extends SearchAndFilterContainer {
public state = {
searchKey: '',
filterClusterVisible: false,
filterStatusVisible: false,
filterTaskVisible: false,
};
public renderColumns = (data: ITaskManage[]) => {
const taskStatus = admin.configsTaskStatus ? admin.configsTaskStatus : [] as IEnumsMap[];
const cluster = Object.assign({
title: '集群名称',
dataIndex: 'clusterName',
key: 'clusterName',
width: '15%',
filters: tableFilter<any>(data, 'clusterName'),
onFilter: (value: string, record: ITaskManage) => record.clusterName.indexOf(value) === 0,
}, this.renderColumnsFilter('filterClusterVisible'));
const status = Object.assign({
title: '任务状态',
dataIndex: 'status',
key: 'status',
width: '15%',
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
onFilter: (value: number, record: ITaskManage) => record.status === +value,
render: (t: number) => {
let messgae: string;
taskStatus.map(ele => {
if (ele.code === t) {
messgae = ele.message;
}
});
return (
<span>{messgae}</span>
);
},
}, this.renderColumnsFilter('filterStatusVisible'));
const taskType = Object.assign({
title: '任务类型',
dataIndex: 'taskType',
key: 'taskType',
width: '15%',
filters: admin.tasksEnums && admin.tasksEnums.map(ele => ({ text: ele.message, value: ele.name })),
onFilter: (value: string, record: ITaskManage) => record.taskType === value,
render: (text: string) => {
const task = admin.tasksEnums && admin.tasksEnums.filter(ele => ele.name === text);
return (<>{task && task[0].message}</>);
},
}, this.renderColumnsFilter('filterTaskVisible'));
return [
{
title: '任务ID',
dataIndex: 'taskId',
key: 'taskId',
width: '15%',
sorter: (a: ITaskManage, b: ITaskManage) => b.taskId - a.taskId,
},
taskType,
cluster,
{
title: '创建时间',
dataIndex: 'gmtCreate',
key: 'gmtCreate',
width: '15%',
sorter: (a: ITaskManage, b: ITaskManage) => b.gmtCreate - a.gmtCreate,
render: (t: number) => moment(t).format(timeFormat),
},
{
title: '操作人',
dataIndex: 'operator',
key: 'operator',
width: '15%',
sorter: (a: ITaskManage, b: ITaskManage) => a.operator.charCodeAt(0) - b.operator.charCodeAt(0),
},
status,
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: '10%',
render: (text: string, record: ITaskManage) => {
return (
<span className="table-operation">
<a href={`${this.urlPrefix}/admin/operation-detail?taskId=${record.taskId}&region=${region.currentRegion}`}></a>
<a href={`${this.urlPrefix}/admin/operation-detail?taskId=${record.taskId}&region=${region.currentRegion}#2`}></a>
</span>
);
},
},
];
}
public getLabelValueData(data: any[]) {
return data.map(item => {
return {
label: item,
value: item,
};
});
}
public getPackages() {
admin.packageList.map(item => {
return {
label: item,
value: item,
};
});
}
public componentDidMount() {
admin.getTaskManagement();
admin.getMetaData(false);
admin.getClusterTasksEnums();
admin.getConfigsTaskStatus();
admin.getConfigsKafkaRoles();
}
public renderOperationPanel() {
return (
<ul>
{this.renderSearch('', '请输入任务ID')}
<li className="right-btn-1">
<Button type="primary" onClick={() => addMigrationTask()}></Button>
</li>
</ul>
);
}
public render() {
const { searchKey } = this.state;
const taskManage: ITaskManage[] = admin.taskManagement && searchKey ?
admin.taskManagement.filter((d: { taskId: number; }) => d.taskId === +searchKey) : admin.taskManagement;
return (
<>
<div className="container">
<div className="table-operation-panel">
{this.renderOperationPanel()}
</div>
<div className="table-wrapper">
<Table
rowKey="taskId"
columns={this.renderColumns(taskManage)}
dataSource={taskManage}
pagination={pagination}
/>
</div>
</div>
</>
);
}
}

View File

@@ -0,0 +1,59 @@
import * as React from 'react';
import { IReassignTasks } from 'types/base-type';
import { Popconfirm } from 'component/antd';
import { urlPrefix } from 'constants/left-menu';
import { startMigrationTask, modifyMigrationTask, cancelMigrationTask } from 'container/modal';
import moment = require('moment');
import { timeFormat } from 'constants/strategy';
export const migrationTaskColumns = (migrationUrl: string) => {
const columns = [{
title: '迁移任务名称',
dataIndex: 'taskName',
render: (text: string, item: IReassignTasks) =>
<a href={`${urlPrefix}/${migrationUrl}?taskId=${item.taskId}`}>{text}</a>,
},
{
title: '创建时间',
dataIndex: 'gmtCreate',
render: (t: number) => moment(t).format(timeFormat),
},
{
title: '创建人',
dataIndex: 'operator',
},
{
title: 'Topic数量',
dataIndex: 'totalTopicNum',
},
{
title: '进度',
dataIndex: 'completedTopicNum',
render: (value: number, item: IReassignTasks) => <span>{item.completedTopicNum}/{item.totalTopicNum}</span>,
},
{
title: '操作',
dataIndex: 'action',
render: (value: string, item: IReassignTasks) => (
<>
{item.status === 0 &&
<Popconfirm
title="确定开始?"
onConfirm={() => startMigrationTask(item, 'start')}
>
<a style={{ marginRight: 16 }}></a>
</Popconfirm>}
{[0, 1].indexOf(item.status) > -1 &&
<a onClick={() => modifyMigrationTask(item, 'modify')} style={{ marginRight: 16 }}></a>}
{item.status === 0 &&
<Popconfirm
title="确定取消?"
onConfirm={() => cancelMigrationTask(item, 'cancel')}
><a></a>
</Popconfirm>}
</>
),
}];
return columns;
};

View File

@@ -0,0 +1,7 @@
.hotspot-header {
margin-bottom: 10px;
}
.hotspot-divider {
margin-top: 5px;
}

View File

@@ -0,0 +1,46 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Tabs } from 'antd';
import { handleTabKey } from 'lib/utils';
import { ClusterTask } from './cluster-task';
import { MigrationTask } from './migration-task';
import { VersionManagement } from '../version-management';
import { users } from 'store/users';
import { expert } from 'store/expert';
const { TabPane } = Tabs;
@observer
export class OperationManagement extends React.Component {
public tabs = [{
title: '迁移任务',
component: <MigrationTask />,
}, {
title: '集群任务',
component: <ClusterTask />,
}, {
title: '版本管理',
component: <VersionManagement />,
}];
public render() {
let tabs = [].concat(this.tabs);
if (users.currentUser.role !== 2) {
tabs = tabs.splice(2);
}
return (
<>
<Tabs activeKey={location.hash.substr(1) || '0'} type="card" onChange={handleTabKey}>
{
tabs.map((item, index) => {
return (
<TabPane tab={item.title} key={'' + index}>
{item.component}
</TabPane>);
})
}
</Tabs>
</>
);
}
}

View File

@@ -0,0 +1,226 @@
import * as React from 'react';
import './index.less';
import { Table, PageHeader, Descriptions, Divider, Tooltip } from 'component/antd';
import { wrapper } from 'store';
import Url from 'lib/url-parser';
import { expert } from 'store/expert';
import { classStatusMap } from 'constants/status-map';
import { admin } from 'store/admin';
import { observer } from 'mobx-react';
import { IXFormWrapper, IReassign, IDetailVO, ILabelValue, IEnumsMap } from 'types/base-type';
import { modifyTransferTask } from 'container/modal';
import { SearchAndFilterContainer } from 'container/search-filter';
import { handlePageBack } from 'lib/utils';
import moment from 'moment';
import './index.less';
import { timeFormat } from 'constants/strategy';
@observer
export class MigrationDetail extends SearchAndFilterContainer {
public taskId: number;
public state = {
filterStatusVisible: false,
};
private xFormModal: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.taskId = Number(url.search.taskId);
}
public showDetails() {
const isUrl = window.location.href.includes('/expert') ? '/expert#2' : '/admin/operation';
const detail = expert.tasksDetail;
const gmtCreate = moment(detail.gmtCreate).format(timeFormat);
const startTime = moment(detail.beginTime).format(timeFormat);
const endTime = moment(detail.endTime).format(timeFormat);
const options = [{
value: detail.taskName,
label: '任务名称',
}, {
value: gmtCreate,
label: '创建时间',
}, {
value: detail.operator,
label: '创建人',
}, {
value: startTime,
label: '计划开始时间',
}, {
value: endTime,
label: '完成时间',
}];
return (
<>
<PageHeader
className="detail hotspot-header"
onBack={() => handlePageBack(isUrl)}
title={`Topic数据迁移任务/${detail.taskName || ''}`}
>
<Divider className="hotspot-divider" />
<Descriptions column={3}>
{options.map((item: ILabelValue, index) => (
<Descriptions.Item key={index} label={item.label}>{item.value}</Descriptions.Item>
))}
<Descriptions.Item label="任务说明">
<Tooltip placement="bottomLeft" title={detail.description}>
<span className="overview">
{detail.description}
</span>
</Tooltip>
</Descriptions.Item>
</Descriptions>
</PageHeader>
</>
);
}
public detailsTable() {
let taskList = [] as IReassign[];
taskList = expert.tasksStatus ? expert.tasksStatus : taskList;
const taskStatus = admin.configsTaskStatus as IEnumsMap[];
const status = Object.assign({
title: '任务状态',
dataIndex: 'status',
key: 'status',
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
onFilter: (value: number, record: IReassign) => record.status === +value,
render: (t: number) => {
let message = '';
taskStatus.forEach((ele: any) => {
if (ele.code === t) {
message = ele.message;
}
});
let statusName = '';
if (t === 100 || t === 101) {
statusName = 'success';
} else if ( t === 40 || t === 99 || t === 102 || t === 103 || t === 104 || t === 105 || t === 106) {
statusName = 'fail';
}
return <span className={statusName}>{message}</span>;
},
}, this.renderColumnsFilter('filterStatusVisible'));
const columns = [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
sorter: (a: IReassign, b: IReassign) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
},
{
title: '所在集群',
dataIndex: 'clusterName',
key: 'clusterName',
},
{
title: '迁移进度',
dataIndex: 'PartitionNum',
key: 'PartitionNum',
render: (text: string, item: IReassign) => <span>{item.completedPartitionNum}/{item.totalPartitionNum}</span>,
},
status,
{
title: '操作',
dataIndex: 'action',
key: 'action',
render: (text: string, item: IReassign) => (
<>
<a onClick={() => this.renderRessignDetail(item)} style={{ marginRight: 16 }}></a>
<a onClick={() => modifyTransferTask(item, 'modify', this.taskId)}></a>
</>
),
},
];
return (
<>
<Table rowKey="key" dataSource={taskList} columns={columns} />
</>
);
}
public renderRessignDetail(item: IReassign) {
let statusList = [] as IDetailVO[];
statusList = item.reassignList ? item.reassignList : statusList;
this.xFormModal = {
type: 'drawer',
noform: true,
nofooter: true,
visible: true,
title: '查看任务状态',
customRenderElement: this.renderInfo(statusList),
width: 500,
onSubmit: () => {
// TODO:
},
};
wrapper.open(this.xFormModal);
}
public renderInfo(statusList: IDetailVO[]) {
const statusColumns = [
{
title: '分区ID',
dataIndex: 'partitionId',
key: 'partitionId',
},
{
title: '目标BrokerID',
dataIndex: 'destReplicaIdList',
key: 'destReplicaIdList',
onCell: () => ({
style: {
maxWidth: 180,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
cursor: 'pointer',
},
}),
render: (t: []) => {
return t.map(i => <span key={i} className="p-params">{i}</span>);
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (t: number) => {
let message = '';
const taskStatus = admin.configsTaskStatus ? admin.configsTaskStatus : [] as IEnumsMap[];
taskStatus.forEach((ele: any) => {
if (ele.code === t) {
message = ele.message;
}
});
return <span className={`${classStatusMap[t]} p-params`}>{message}</span>;
},
},
];
return (
<Table rowKey="key" dataSource={statusList} columns={statusColumns} />
);
}
public componentDidMount() {
expert.getReassignTasksDetail(this.taskId);
expert.getReassignTasksStatus(this.taskId);
admin.getConfigsTaskStatus();
}
public render() {
return (
expert.tasksDetail ?
(
<>
{this.showDetails()}
{admin.configsTaskStatus ? this.detailsTable() : null}
</>
) : null
);
}
}

View File

@@ -0,0 +1,110 @@
import * as React from 'react';
import { IReassignTasks, IEnumsMap } from 'types/base-type';
import { Table, Button } from 'component/antd';
import { expert } from 'store/expert';
import { pagination } from 'constants/table';
import { observer } from 'mobx-react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { createMigrationTasks } from 'container/modal';
import { admin } from 'store/admin';
import { migrationTaskColumns } from './config';
import './index.less';
@observer
export class MigrationTask extends SearchAndFilterContainer {
public state = {
searchKey: '',
filterStatusVisible: false,
};
public getColumns = () => {
const columns = migrationTaskColumns(window.location.href.includes('/expert') ? 'expert/hotspot-detail' : 'admin/migration-detail');
const taskStatus = admin.configsTaskStatus as IEnumsMap[];
const status = Object.assign({
title: '任务状态',
dataIndex: 'status',
key: 'status',
filters: taskStatus.map(ele => ({ text: ele.message, value: ele.code + '' })),
onFilter: (value: number, record: IReassignTasks) => record.status === +value,
render: (t: number) => {
let message = '';
taskStatus.forEach((ele: any) => {
if (ele.code === t) {
message = ele.message;
}
});
let statusName = '';
if (t === 100 || t === 101) {
statusName = 'success';
} else if (t === 40 || t === 99 || t === 102 || t === 103 || t === 104 || t === 105 || t === 106) {
statusName = 'fail';
}
return <span className={statusName}>{message}</span>;
},
}, this.renderColumnsFilter('filterStatusVisible'));
const col = columns.splice(4, 0, status);
return columns;
}
public getMigrationTask() {
return (
<>
<Table
columns={this.getColumns()}
dataSource={expert.reassignTasks}
pagination={pagination}
/>
</>
);
}
public getData(data: IReassignTasks[]) {
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
const reassignTasks: IReassignTasks[] = data.filter(d =>
(d.taskName !== undefined && d.taskName !== null) && d.taskName.toLowerCase().includes(searchKey as string));
return reassignTasks;
}
public componentDidMount() {
expert.getReassignTasks();
if (!expert.metaData.length) {
expert.getMetaData(false);
}
admin.getConfigsTaskStatus();
}
public renderOperationPanel() {
return (
<ul>
{this.renderSearch('', '请输入任务名称')}
{location.pathname.includes('expert') ? null : <li className="right-btn-1">
<Button type="primary" onClick={() => createMigrationTasks()}></Button>
</li>}
</ul>
);
}
public render() {
if (!admin.configsTaskStatus) {
return null;
}
return (
<>
<div className="container">
<div className="table-operation-panel">
{this.renderOperationPanel()}
</div>
<div className="table-wrapper">
<Table
columns={this.getColumns()}
dataSource={this.getData(expert.reassignTasks)}
pagination={pagination}
/>
</div>
</div>
</>
);
}
}

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Tabs } from 'antd';
import { handleTabKey } from 'lib/utils';
import { AdminAppList } from './admin-app-list';
import { UserManagement } from './user-management';
import { ConfigureManagement } from './configure-management';
const { TabPane } = Tabs;
@observer
export class PlatformManagement extends React.Component {
public render() {
return (
<>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="应用管理" key="1">
<AdminAppList />
</TabPane>
<TabPane tab="用户管理" key="2">
<UserManagement />
</TabPane>
<TabPane tab="配置管理" key="3">
<ConfigureManagement />
</TabPane>
</Tabs>
</>
);
}
}

View File

@@ -0,0 +1,88 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { Table, Button, Spin } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { IUser } from 'types/base-type';
import { users } from 'store/users';
import { pagination } from 'constants/table';
import { getUserColumns } from './config';
import { showApplyModal } from 'container/modal/admin';
import { roleMap } from 'constants/status-map';
import { tableFilter } from 'lib/utils';
@observer
export class UserManagement extends SearchAndFilterContainer {
public state = {
searchKey: '',
filterRole: false,
};
public componentDidMount() {
if (!users.userData.length) {
users.getUserList();
}
}
public getData<T extends IUser>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IUser) =>
(item.username !== undefined && item.username !== null) && item.username.toLowerCase().includes(searchKey as string)) : origin ;
return data;
}
public renderTable() {
const roleColumn = Object.assign({
title: '角色权限',
dataIndex: 'role',
key: 'role',
width: '35%',
filters: tableFilter<IUser>(users.userData, 'role', roleMap),
onFilter: (text: number, record: IUser) => record.role === text,
render: (text: number) => roleMap[text] || '',
}, this.renderColumnsFilter('filterRole')) as any;
const userColumns = getUserColumns();
userColumns.splice(1, 0, roleColumn);
return (
<Spin spinning={users.loading}>
<Table
rowKey="key"
columns={userColumns}
dataSource={this.getData(users.userData)}
pagination={pagination}
/>
</Spin>
);
}
public renderOperationPanel() {
return (
<ul>
{this.renderSearch('', '请输入用户名或应用名称')}
<li className="right-btn-1">
<Button type="primary" onClick={() => showApplyModal()}></Button>
</li>
</ul>
);
}
public render() {
return (
<div className="container">
<div className="table-operation-panel">
{this.renderOperationPanel()}
</div>
<div className="table-wrapper">
{this.renderTable()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,105 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { Table, Button, Spin } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { IUploadFile } from 'types/base-type';
import { version } from 'store/version';
import { pagination } from 'constants/table';
import { getVersionColumns } from './config';
import { showUploadModal } from 'container/modal/admin';
import { tableFilter } from 'lib/utils';
import { admin } from 'store/admin';
@observer
export class VersionManagement extends SearchAndFilterContainer {
public state = {
searchKey: '',
filterClusterNameVisible: false,
filterConfigTypeVisible: false,
};
public async componentDidMount() {
if (!version.fileTypeList.length) {
await version.getFileTypeList();
}
if (!version.fileList.length) {
version.getFileList();
}
if (!admin.metaList.length) {
admin.getMetaData(false);
}
}
public getColumns = () => {
const columns = getVersionColumns();
const clusterName = Object.assign({
title: '集群名称',
dataIndex: 'clusterName',
key: 'clusterName',
filters: tableFilter<any>(this.getData(version.fileList), 'clusterName'),
onFilter: (value: string, record: IUploadFile) => record.clusterName === value,
}, this.renderColumnsFilter('filterClusterNameVisible'));
const configType = Object.assign({
title: '配置类型',
dataIndex: 'configType',
key: 'configType',
filters: tableFilter<any>(this.getData(version.fileList), 'configType'),
onFilter: (value: string, record: IUploadFile) => record.configType === value,
}, this.renderColumnsFilter('filterConfigTypeVisible'));
const col = columns.splice(1, 0, clusterName, configType);
return columns;
}
public getData<T extends IUploadFile>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
if (searchKey) {
data = origin.filter((item: IUploadFile) => item.id + '' === searchKey
|| ((item.fileName !== undefined && item.fileName !== null) && item.fileName.toLowerCase().includes(searchKey as string)));
}
return data;
}
public renderTable() {
return (
<Spin spinning={version.loading}>
<Table
rowKey="key"
columns={this.getColumns()}
dataSource={this.getData(version.fileList)}
pagination={pagination}
/>
</Spin>
);
}
public renderOperationPanel() {
return (
<ul>
{this.renderSearch('', '请输入ID或文件名')}
<li className="right-btn-1">
<Button type="primary" onClick={() => showUploadModal()}></Button>
</li>
</ul>
);
}
public render() {
const currentFileType = version.currentFileType;
const acceptFileMap = version.acceptFileMap;
return (
<div className="container">
<div className="table-operation-panel">
{this.renderOperationPanel()}
</div>
<div className="table-wrapper">
{this.renderTable()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,57 @@
import { XFormComponent } from 'component/x-form';
import { xActionFormMap } from './config';
import * as React from 'react';
import { IRequestParams, IStrategyAction, IConfigForm } from 'types/alarm';
export class ActionForm extends React.Component {
public $form: any = null;
public getFormData() {
let configValue = null as IConfigForm;
this.$form.validateFields((error: Error, result: IConfigForm) => {
if (error) {
return;
}
configValue = result;
});
return configValue;
}
public resetFormData() {
this.$form.resetFields();
}
public updateFormData(monitorRule: IRequestParams) {
const strategyAction = monitorRule.strategyActionList[0] || {} as IStrategyAction;
this.$form.setFieldsValue({
level: monitorRule.priority,
alarmPeriod: strategyAction.converge.split(',')[0],
alarmTimes: strategyAction.converge.split(',')[1],
acceptGroup: strategyAction.notifyGroup,
callback: strategyAction.callback,
});
}
public render() {
const formData = {};
const formLayout = {
labelCol: { span: 3 },
wrapperCol: { span: 12 },
};
return (
<div className="config-wrapper">
<span className="span-tag"></span>
<div className="alarm-x-form action-form">
<XFormComponent
ref={form => this.$form = form}
formData={formData}
formMap={xActionFormMap}
formLayout={formLayout}
/>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,60 @@
import * as React from 'react';
import { alarm } from 'store/alarm';
import { IMonitorGroups } from 'types/base-type';
import { getValueFromLocalStorage, setValueToLocalStorage } from 'lib/local-storage';
import { VirtualScrollSelect } from '../../../component/virtual-scroll-select';
interface IAlarmSelectProps {
onChange?: (result: string[]) => any;
value?: string[];
isDisabled?: boolean;
}
export class AlarmSelect extends React.Component<IAlarmSelectProps> {
public getData = async () => {
const originMonitorList = getValueFromLocalStorage('monitorGroups');
if (originMonitorList) return originMonitorList;
return await this.fetchMonitor();
}
public fetchMonitor = async () => {
let data = await alarm.getMonitorGroups();
data = (data || []).map((item: IMonitorGroups) => {
return {
...item,
label: item.name,
value: item.name,
};
});
setValueToLocalStorage('monitorGroups', data);
return data;
}
public handleChange = (params: string[]) => {
const { onChange } = this.props;
// tslint:disable-next-line:no-unused-expression
onChange && onChange(params);
}
public render() {
const { value, isDisabled } = this.props;
return (
<>
<VirtualScrollSelect
attrs={{ mode: 'multiple', placeholder: '请选择报警接收组' }}
value={value}
isDisabled={isDisabled}
getData={this.getData}
onChange={this.handleChange}
/>
<a
className="icon-color"
target="_blank"
href="https://github.com/didi/kafka-manager"
>
</a>
</>
);
}
}

View File

@@ -0,0 +1,246 @@
import * as React from 'react';
import { Tooltip, notification, Radio, Icon, Popconfirm, RadioChangeEvent } from 'component/antd';
import { IMonitorStrategies, ILabelValue } from 'types/base-type';
import { IFormItem, IFormSelect } from 'component/x-form';
import { AlarmSelect } from 'container/alarm/add-alarm/alarm-select';
import { weekOptions } from 'constants/status-map';
import { alarm } from 'store/alarm';
import { app } from 'store/app';
import moment from 'moment';
import { cellStyle } from 'constants/table';
import { timeFormat } from 'constants/strategy';
import { region } from 'store/region';
export const getAlarmColumns = (urlPrefix: string) => {
const columns = [
{
title: '告警名称',
dataIndex: 'name',
key: 'name',
width: '25%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => a.name.charCodeAt(0) - b.name.charCodeAt(0),
render: (text: string, record: IMonitorStrategies) => {
return (
<Tooltip placement="bottomLeft" title={record.name} >
<a href={`${urlPrefix}/alarm/alarm-detail?id=${record.id}&region=${region.currentRegion}`}> {text} </a>
</Tooltip>);
},
}, {
title: '应用名称',
dataIndex: 'appName',
key: 'appName',
width: '25%',
onCell: () => ({
style: {
maxWidth: 250,
...cellStyle,
},
}),
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => a.appName.charCodeAt(0) - b.appName.charCodeAt(0),
render: (text: string, record: IMonitorStrategies) =>
<Tooltip placement="bottomLeft" title={record.principals} >{text}</Tooltip>,
}, {
title: '操作人',
dataIndex: 'operator',
key: 'operator',
width: '20%',
onCell: () => ({
style: {
maxWidth: 100,
...cellStyle,
},
}),
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => a.operator.charCodeAt(0) - b.operator.charCodeAt(0),
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
}, {
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: '20%',
sorter: (a: IMonitorStrategies, b: IMonitorStrategies) => b.createTime - a.createTime,
render: (time: number) => moment(time).format(timeFormat),
}, {
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: '10%',
render: (text: string, item: IMonitorStrategies) => (
<>
<a href={`${urlPrefix}/alarm/modify?id=${item.id}`} className="action-button"></a>
<Popconfirm
title="确定删除?"
onConfirm={() => deteleMonitor(item)}
>
<a></a>
</Popconfirm>
</>
),
},
];
return columns;
};
export const getRandomKey = () => {
return (new Date()).getTime();
};
export const deteleMonitor = (item: IMonitorStrategies) => {
alarm.deteleMonitorStrategies(item.id).then(data => {
notification.success({ message: '删除成功' });
});
};
export const getAlarmTime = () => {
const timeOptions = [] as ILabelValue[];
const defaultTime = [] as number[];
for (let i = 0; i < 24; i++) {
timeOptions.push({
label: `${i}`,
value: i,
});
defaultTime.push(i);
}
return { timeOptions, defaultTime };
};
export const getAlarmWeek = () => {
const defWeek = [] as number[];
for (let i = 0; i < 7; i++) {
defWeek.push(i);
}
return { defWeek, weekOptions };
};
interface IRadioProps {
onChange?: (result: number) => any;
value?: number;
}
const isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
class RadioIcon extends React.Component<IRadioProps> {
public onRadioChange = (e: RadioChangeEvent) => {
const { onChange } = this.props;
if (onChange) {
onChange(e.target.value);
}
}
public render() {
const { value } = this.props;
return (
<Radio.Group
name="radiogroup"
value={value || 3}
disabled={isDetailPage}
onChange={this.onRadioChange}
>
<Radio value={1} key={1}>
<Icon type="phone" />
<Icon type="message" />
<Icon type="mail" />
<Icon type="dingding" />
</Radio>
<Radio value={2} key={2}>
<Icon type="message" />
<Icon type="mail" />
<Icon type="dingding" />
</Radio>
<Radio value={3} key={3}>
<Icon type="mail" />
<Icon type="dingding" />
</Radio>
</Radio.Group>
);
}
}
export const xActionFormMap = [{
key: 'level',
label: '报警级别',
type: 'custom',
defaultValue: 3,
customFormItem: <RadioIcon />,
rules: [{ required: true, message: '请输入报警接收组' }],
}, {
key: 'alarmPeriod',
label: '报警周期(分钟)',
type: 'input_number',
attrs: {
min: 0,
disabled: isDetailPage,
},
rules: [{ required: true, message: '请输入报警周期' }],
}, {
key: 'alarmTimes',
label: '周期内报警次数',
type: 'input_number',
attrs: {
min: 0,
disabled: isDetailPage,
},
rules: [{ required: true, message: '请输入周期内报警次数' }],
}, {
key: 'acceptGroup',
label: '报警接收组',
type: 'custom',
customFormItem: <AlarmSelect isDisabled={isDetailPage}/>,
rules: [{ required: true, message: '请输入报警接收组' }],
},
{
key: 'callback',
label: '回调地址',
rules: [{ required: false, message: '请输入回调地址' }],
attrs: {disabled: isDetailPage},
}] as unknown as IFormSelect[]; // as IFormItem[];
export const xTypeFormMap = [{
key: 'alarmName',
label: '告警名称',
rules: [{ required: true, message: '请输入告警名称' }],
attrs: {placeholder: '请输入', disabled: isDetailPage},
}, {
key: 'app',
label: '所属应用',
type: 'select',
attrs: {
placeholder: '请选择',
optionFilterProp: 'children',
showSearch: true,
filterOption: (input: any, option: any) => {
if ( typeof option.props.children === 'object' ) {
const { props } = option.props.children as any;
return (props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
}
return (option.props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
onChange: (e: string) => app.changeActiveApp(e),
disabled: isDetailPage,
},
rules: [{ required: true, message: '请输入报警接收组' }],
}] as unknown as IFormSelect[];
export const xTimeFormMap = [{
key: 'weeks',
label: '每周',
type: 'check_box',
defaultValue: getAlarmWeek().defWeek,
options: getAlarmWeek().weekOptions,
rules: [{ required: true, message: '请选择' }],
}, {
key: 'hours',
label: '每天',
type: 'check_box',
defaultValue: getAlarmTime().defaultTime,
options: getAlarmTime().timeOptions,
rules: [{ required: true, message: '请选择' }],
}] as unknown as IFormSelect[];

View File

@@ -0,0 +1,391 @@
import * as React from 'react';
import { Select, Spin, Form, Tooltip } from 'component/antd';
import { message } from 'antd';
import { IFormItem } from 'component/x-form';
import { cluster } from 'store/cluster';
import { alarm } from 'store/alarm';
import { topic } from 'store/topic';
import { observer } from 'mobx-react';
import { IRequestParams, IStrategyFilter } from 'types/alarm';
import { filterKeys } from 'constants/strategy';
import { VirtualScrollSelect } from 'component/virtual-scroll-select';
import { IsNotNaN } from 'lib/utils';
import { searchProps } from 'constants/table';
interface IDynamicProps {
form?: any;
formData?: any;
}
interface IFormSelect extends IFormItem {
options: Array<{ key?: string | number, value: string | number, label: string }>;
}
interface IVritualScrollSelect extends IFormSelect {
getData: () => any;
isDisabled: boolean;
refetchData?: boolean;
}
@observer
export class DynamicSetFilter extends React.Component<IDynamicProps> {
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
public monitorType: string = null;
public clusterId: number = null;
public clusterName: string = null;
public topicName: string = null;
public consumerGroup: string = null;
public location: string = null;
public getFormValidateData() {
const filterList = [] as IStrategyFilter[];
let monitorType = '' as string;
let filterObj = {} as any;
this.props.form.validateFields((err: Error, values: any) => {
if (!err) {
monitorType = values.monitorType;
const index = cluster.clusterData.findIndex(item => item.clusterId === values.cluster);
if (index > -1) {
values.clusterName = cluster.clusterData[index].clusterName;
}
for (const key of Object.keys(values)) {
if (filterKeys.indexOf(key) > -1) { // 只有这几种值可以设置
filterList.push({
tkey: key === 'clusterName' ? 'cluster' : key, // 传参需要将clusterName转成cluster
topt: '=',
tval: [values[key]],
});
}
}
}
});
return filterObj = {
monitorType,
filterList,
};
}
public resetForm() {
const { resetFields } = this.props.form;
this.clearFormData();
resetFields();
}
public resetFormValue(
monitorType: string = null,
clusterId: number = null,
topicName: string = null,
consumerGroup: string = null,
location: string = null) {
const { setFieldsValue } = this.props.form;
setFieldsValue({
cluster: clusterId,
topic: topicName,
consumerGroup,
location,
monitorType,
});
}
public getClusterId = (clusterName: string) => {
let clusterId = null;
const index = cluster.clusterData.findIndex(item => item.clusterName === clusterName);
if (index > -1) {
clusterId = cluster.clusterData[index].clusterId;
}
if (clusterId) {
cluster.getClusterMetaTopics(clusterId);
this.clusterId = clusterId;
return this.clusterId;
}
return this.clusterId = clusterName as any;
}
public async initFormValue(monitorRule: IRequestParams) {
const strategyFilterList = monitorRule.strategyFilterList;
const clusterFilter = strategyFilterList.filter(item => item.tkey === 'cluster')[0];
const topicFilter = strategyFilterList.filter(item => item.tkey === 'topic')[0];
const consumerFilter = strategyFilterList.filter(item => item.tkey === 'consumerGroup')[0];
const clusterName = clusterFilter ? clusterFilter.tval[0] : null;
const topic = topicFilter ? topicFilter.tval[0] : null;
const consumerGroup = consumerFilter ? consumerFilter.tval[0] : null;
const location: string = null;
const monitorType = monitorRule.strategyExpressionList[0].metric;
alarm.changeMonitorStrategyType(monitorType);
await this.getClusterId(clusterName);
await this.handleSelectChange(topic, 'topic');
await this.handleSelectChange(consumerGroup, 'consumerGroup');
this.resetFormValue(monitorType, this.clusterId, topic, consumerGroup, location);
}
public clearFormData() {
this.monitorType = null;
this.topicName = null;
this.clusterId = null;
this.consumerGroup = null;
this.location = null;
this.resetFormValue();
}
public async handleClusterChange(e: number) {
this.clusterId = e;
this.topicName = null;
topic.setLoading(true);
await cluster.getClusterMetaTopics(e);
this.resetFormValue(this.monitorType, e, null, this.consumerGroup, this.location);
topic.setLoading(false);
}
public handleSelectChange = (e: string, type: 'topic' | 'consumerGroup' | 'location') => {
switch (type) {
case 'topic':
if (!this.clusterId) {
return message.info('请选择集群');
}
this.topicName = e;
const type = this.dealMonitorType();
if (['kafka-consumer-maxLag', 'kafka-consumer-maxDelayTime', 'kafka-consumer-lag'].indexOf(type) > -1) {
this.getConsumerInfo();
}
break;
case 'consumerGroup':
this.consumerGroup = e;
break;
case 'location':
this.location = e;
break;
}
}
public getConsumerInfo = () => {
if (!this.clusterId || !this.topicName) {
return;
}
topic.setLoading(true);
if (IsNotNaN(this.clusterId)) {
topic.getConsumerGroups(this.clusterId, this.topicName);
}
this.consumerGroup = null;
this.location = null;
this.resetFormValue(this.monitorType, this.clusterId, this.topicName);
topic.setLoading(false);
}
public dealMonitorType() {
const index = alarm.monitorType.indexOf('-');
let type = alarm.monitorType;
if (index > -1) {
type = type.substring(index + 1);
}
return type;
}
public getRenderItem() {
const type = this.dealMonitorType();
const showMore = ['kafka-consumer-maxLag', 'kafka-consumer-maxDelayTime', 'kafka-consumer-lag'].indexOf(type) > -1;
this.monitorType = alarm.monitorType;
const monitorType = {
key: 'monitorType',
label: '监控指标',
type: 'select',
options: alarm.monitorTypeList.map(item => ({
label: item.metricName,
value: item.metricName,
})),
attrs: {
placeholder: '请选择',
className: 'large-size',
disabled: this.isDetailPage,
optionFilterProp: 'children',
showSearch: true,
filterOption: (input: any, option: any) => {
if (typeof option.props.children === 'object') {
const { props } = option.props.children as any;
return (props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
}
return (option.props.children + '').toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
onChange: (e: string) => this.handleTypeChange(e),
},
rules: [{ required: true, message: '请选择监控指标' }],
} as IVritualScrollSelect;
const clusterItem = {
label: '集群',
options: cluster.clusterData,
defaultValue: this.clusterId,
rules: [{ required: true, message: '请选择集群' }],
attrs: {
placeholder: '请选择集群',
className: 'middle-size',
disabled: this.isDetailPage,
onChange: (e: number) => this.handleClusterChange(e),
},
key: 'cluster',
} as unknown as IVritualScrollSelect;
const topicItem = {
label: 'Topic',
defaultValue: this.topicName,
rules: [{ required: true, message: '请选择Topic' }],
isDisabled: this.isDetailPage,
options: cluster.clusterMetaTopics.map(item => {
return {
label: item.topicName,
value: item.topicName,
};
}),
attrs: {
placeholder: '请选择Topic',
className: 'middle-size',
disabled: this.isDetailPage,
onChange: (e: string) => this.handleSelectChange(e, 'topic'),
},
key: 'topic',
} as IVritualScrollSelect;
const consumerGroupItem = {
label: '消费组',
options: topic.consumerGroups.map(item => {
return {
label: item.consumerGroup,
value: item.consumerGroup,
};
}),
defaultValue: this.consumerGroup,
rules: [{ required: showMore, message: '请选择消费组' }],
attrs: {
placeholder: '请选择消费组',
className: 'middle-size',
disabled: this.isDetailPage,
onChange: (e: string) => this.handleSelectChange(e, 'consumerGroup'),
},
key: 'consumerGroup',
} as IVritualScrollSelect;
const locationItem = {
label: 'location',
options: topic.filterGroups.map(item => {
return {
label: item.location,
value: item.location,
};
}),
defaultValue: this.location,
rules: [{ required: showMore, message: '请选择location' }],
attrs: {
placeholder: '请选择location',
optionFilterProp: 'children',
showSearch: true,
className: 'middle-size',
disabled: this.isDetailPage,
onChange: (e: string) => this.handleSelectChange(e, 'location'),
},
key: 'location',
} as IVritualScrollSelect;
const common = (
<>
{this.renderFormItem(clusterItem)}
{this.renderFormItem(topicItem)}
</>
);
const more = showMore ? (
<>
{this.renderFormItem(consumerGroupItem)}
{/* {this.renderFormItem(locationItem)} */}
</>
) : null;
return (
<>
<div className="dynamic-set">
{this.renderFormItem(monitorType)}
<ul>{common}{more}</ul>
</div>
</>
);
}
public handleTypeChange = (e: string) => {
// tslint:disable-next-line:no-unused-expression
this.clearFormData();
alarm.changeMonitorStrategyType(e);
}
public getSelectFormItem(item: IFormItem) {
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 > 25 ? <Tooltip placement="bottomLeft" title={v.label}>
{v.label}
</Tooltip> : v.label}
</Select.Option>
))}
</Select>
);
}
public renderFormItem(item: IVritualScrollSelect, virtualScroll: boolean = false) {
const { getFieldDecorator } = this.props.form;
const { formData = {} } = this.props;
const initialValue = formData[item.key] === 0 ? 0 : (formData[item.key] || item.defaultValue || '');
const getFieldValue = {
initialValue,
rules: item.rules || [{ required: true, message: '请填写' }],
};
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 10 },
};
return (
<Form.Item
label={item.label}
key={item.key}
{...formItemLayout}
>
{getFieldDecorator(item.key, getFieldValue)(
virtualScroll ?
<VirtualScrollSelect
attrs={item.attrs}
isDisabled={item.isDisabled}
onChange={item.attrs.onChange}
getData={item.getData}
refetchData={item.refetchData}
/>
: this.getSelectFormItem(item),
)}
</Form.Item>
);
}
public componentDidMount() {
cluster.getClusters();
}
public render() {
return (
<Spin spinning={cluster.filterLoading}>
<Form>
<div className="form-list">
{this.getRenderItem()}
</div>
</Form>
</Spin>
);
}
}
export const WrappedDynamicSetFilter = Form.create({ name: 'dynamic_filter_form_item' })(DynamicSetFilter);

View File

@@ -0,0 +1,224 @@
.btn-group {
background: white;
width: calc(100% - 215px);
position: fixed;
top: 75px;
right: 22px;
z-index: 999999;
box-shadow: 0px 12px 8px -14px #c5c2c2;
}
.container_box{
width: 100%;
margin-top: 65px;
}
.config-wrapper {
background: white;
height: 100%;
padding-left: 20px;
.alarm-time-form{
border-top: 1px solid #E8E8E8;
height: 80px;
padding: 10px 0 20px;
margin-bottom: 20px;
.form-item{
float: left;
}
b{
float: left;
font-size: 13px;
margin: 0 5px;
font-weight: 100;
line-height: 38px;
}
}
.alarm-x-form {
border-top: 1px solid #E8E8E8;
padding-bottom: 20px;
margin-bottom: 20px;
Icon {
margin-left: 8px;
}
&.type-form {
padding-top: 10px;
.ant-form-item {
width: 30%
}
.ant-form-item-label {
padding-left: 10px;
}
.ant-form-item-control {
width: 220px;
}
}
&.action-form {
.ant-col-3 {
text-align: left;
padding-left: 10px;
}
.anticon {
margin-left: 8px;
}
}
}
.span-tag {
border-left: 2px solid @primary-color;
padding-left: 8px;
font-size: 14px;
line-height: 40px;
font-family: PingFangSC-Regular;
}
.info-wrapper {
border-top: 1px solid #E8E8E8;
margin-bottom: 20px;
padding: 15px 10px;
ul {
display: flex;
li {
flex: 1;
vertical-align: middle;
margin-right: 15px;
.ant-select {
margin-left: 15px;
width: 200px;
}
}
}
.ant-form {
border: 1px dashed #e29864;
padding: 20px 0px 20px 10px;
margin-bottom: 20px;
}
.form-list {
line-height: 40px;
display: inline-block;
}
.ant-form-item {
display: inline-block;
margin: 0px 5px 10px 5px;
.ant-select {
width: 150px;
margin-right: 5px;
&.small-size {
width: 100px;
}
&.middle-size {
width: 150px;
}
&.large-size {
width: 300px;
}
}
.ant-input-number {
width: 100px;
margin-left: 5px;
margin-right: 5px;
}
}
.dynamic-button {
font-size: 16px;
transition: all 0.3s;
margin-left: 15px;
}
.dynamic-button:hover {
&.delete {
color: red;
}
&.plus {
color: green;
}
}
.dynamic-button[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
}
}
.dynamic-set {
padding: 15px 10px;
ul{
li{
float: left;
}
}
.form-list {
line-height: 40px;
display: inline-block;
}
.ant-form-item {
display: inline-block;
margin: 0px 5px 10px 5px;
.ant-select {
width: 150px;
margin-right: 5px;
&.small-size {
width: 100px;
}
&.middle-size {
width: 190px;
}
&.large-size {
width: 300px;
}
}
}
}
.strategy {
display: inline-block;
width: 90%;
border: 1px dashed #dcc4af;
padding: 15px 15px;
margin: 0px 15px;
&:first-child {
margin-top: 15px;
}
.content {
display: inline-block;
width: 80%;
.time-select {
width: 50%;
margin-right: 20px;
}
}
}
.is-show{
display: none;
}

View File

@@ -0,0 +1,171 @@
import * as React from 'react';
import './index.less';
import { WrappedDynamicSetStrategy } from './strategy-form';
import { Button, PageHeader, Spin, message } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { WrappedTimeForm } from './time-form';
import { ActionForm } from './action-form';
import { TypeForm } from './type-form';
import { handlePageBack } from 'lib/utils';
import { observer } from 'mobx-react';
import { alarm } from 'store/alarm';
import { app } from 'store/app';
import Url from 'lib/url-parser';
import { IStrategyExpression, IRequestParams } from 'types/alarm';
@observer
export class AddAlarm extends SearchAndFilterContainer {
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
public strategyForm: any = null;
public actionForm: any = null;
public timeForm: any = null;
public typeForm: any = null;
public id: number = null;
constructor(props: any) {
super(props);
const url = Url();
this.id = Number(url.search.id);
}
public async componentDidMount() {
alarm.getMonitorType();
alarm.setLoading(true);
app.getAppList();
if (this.id || this.id === 0) {
await alarm.getMonitorDetail(this.id);
this.initMonitorDetailData();
}
alarm.setLoading(false);
}
public initMonitorDetailData() {
if (alarm.monitorStrategyDetail.monitorRule) {
if (!this.strategyForm || !this.actionForm || !this.typeForm || !this.timeForm) {
return;
}
const monitorRule = alarm.monitorStrategyDetail.monitorRule || {} as IRequestParams;
this.timeForm.updateFormData(monitorRule);
this.typeForm.updateFormData(monitorRule);
this.actionForm.updateFormData(monitorRule);
this.strategyForm.updateFormValue(monitorRule);
}
}
public handleSubmit = () => {
if (!this.strategyForm || !this.actionForm || !this.typeForm || !this.timeForm) {
return;
}
const params = this.generateRequestParams() as IRequestParams;
if (!params) return;
(this.id || this.id === 0) ?
alarm.modifyMonitorStrategy({ id: this.id, ...params }) : alarm.addMonitorStategy(params);
}
public handleResetForm = (id?: number) => {
if (id || id === 0) {
alarm.getMonitorDetail(this.id);
this.initMonitorDetailData();
} else {
if (!this.strategyForm || !this.actionForm || !this.typeForm || !this.timeForm) {
return;
}
this.typeForm.resetFormData();
this.timeForm.resetFormData();
this.actionForm.resetFormData();
this.strategyForm.resetForm();
}
}
public generateRequestParams() {
const actionValue = this.actionForm.getFormData();
const timeValue = this.timeForm.getFormData();
const typeValue = this.typeForm.getFormData().typeValue;
let strategyList = this.strategyForm.getFormValidateData();
const filterObj = this.typeForm.getFormData().filterObj;
// tslint:disable-next-line:max-line-length
if (!actionValue || !timeValue || !typeValue || !strategyList.length || !filterObj || !filterObj.filterList.length) {
message.error('请正确填写必填项');
return null;
}
if (filterObj.monitorType === 'online-kafka-topic-throttled') {
filterObj.filterList.push({
tkey: 'app',
topt: '=',
tval: [typeValue.app],
});
}
strategyList = strategyList.map((row: IStrategyExpression) => {
return {
...row,
metric: filterObj.monitorType,
};
});
return {
appId: typeValue.app,
name: typeValue.alarmName,
periodDaysOfWeek: timeValue.weeks.join(','),
periodHoursOfDay: timeValue.hours.join(','),
priority: actionValue.level,
strategyActionList: [{
callback: actionValue.callback,
notifyGroup: actionValue.acceptGroup,
converge: actionValue.alarmPeriod + ',' + actionValue.alarmTimes,
type: 'notify',
sendRecovery: 1,
}],
strategyExpressionList: strategyList,
strategyFilterList: filterObj.filterList,
} as IRequestParams;
}
public renderAlarmStrategy() {
return (
<div className="config-wrapper">
<span className="span-tag"></span>
<div className="info-wrapper">
<WrappedDynamicSetStrategy wrappedComponentRef={(form: any) => this.strategyForm = form} />
</div>
</div>
);
}
public renderTimeForm() {
return (
<>
<WrappedTimeForm wrappedComponentRef={(form: any) => this.timeForm = form} />
</>
);
}
public render() {
return (
<Spin spinning={alarm.loading}>
<div className={this.isDetailPage ? '' : 'container_box'}>
<PageHeader
className={this.isDetailPage ? 'is-show' : 'btn-group'}
onBack={() => handlePageBack('/alarm')}
title={(this.id || this.id === 0) ? '修改告警配置' : '新建告警配置'}
extra={[
<Button key="1" type="primary" onClick={() => this.handleSubmit()}></Button>,
<Button key="2" onClick={() => this.handleResetForm(this.id)}></Button>,
]}
/>
<TypeForm
ref={(form) => this.typeForm = form}
/>
{this.renderAlarmStrategy()}
{this.renderTimeForm()}
<ActionForm ref={(actionForm) => this.actionForm = actionForm} />
</div>
</Spin>
);
}
}

View File

@@ -0,0 +1,369 @@
import * as React from 'react';
import { Icon, InputNumber, Select, message, Form, Tooltip } from 'component/antd';
import { equalList, funcKeyMap, funcList } from 'constants/strategy';
import { IStringMap } from 'types/base-type';
import { IRequestParams } from 'types/alarm';
import { IFormSelect, IFormItem, FormItemType } from 'component/x-form';
import { searchProps } from 'constants/table';
interface IDynamicProps {
form: any;
formData?: any;
maxLimit?: number;
}
interface ICRUDItem {
id: string;
func: string;
eopt?: string;
threshold?: number;
period?: number;
count?: number;
day?: number;
}
const commonKeys = ['eopt', 'threshold', 'func'];
class DynamicSetStrategy extends React.Component<IDynamicProps> {
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
public crudList = [] as ICRUDItem[];
public state = {
shouldUpdate: false,
};
public componentDidMount() {
if (!this.crudList.length) {
const id = `0_`;
this.crudList.push({
id,
func: 'happen',
});
}
this.updateRender();
}
public updateRender() {
this.setState({
shouldUpdate: !this.state.shouldUpdate,
});
}
public resetForm() {
const { resetFields } = this.props.form;
resetFields();
}
public dealFormParams(monitorRule: IRequestParams) {
const initialCrudList = [] as ICRUDItem[];
if (monitorRule.strategyExpressionList) {
const expressionList = monitorRule.strategyExpressionList;
expressionList.map((row: any, index) => {
const obj = {} as any;
for (const key of commonKeys) {
obj[key] = row[key];
}
const otherKeys = funcKeyMap[row.func] as string[];
// 除去commonKeys中的key 其他值在提交时全塞到params中 故在编辑详情渲染时再拆回来
const parmas = row.params ? row.params.split(',').map((row: string) => +row) : [];
otherKeys.forEach((line: string, i: number) => {
obj[line] = parmas[i] || 0;
});
obj.id = `${index}_`;
initialCrudList.push(obj);
});
}
return initialCrudList;
}
public updateFormValue(monitorRule: IRequestParams) {
const { setFieldsValue } = this.props.form;
const initialCrudList = this.dealFormParams(monitorRule);
if (!initialCrudList.length) return;
const filledKeys = ['period'].concat(commonKeys);
const formKeyMap = {
happen: ['count'].concat(filledKeys),
ndiff: ['count'].concat(filledKeys),
all: [].concat(filledKeys),
pdiff: [].concat(filledKeys),
sum: [].concat(filledKeys),
c_avg_rate_abs: ['day'].concat(filledKeys),
} as {
[key: string]: string[],
};
const feildValue = {
} as any;
for (const item of initialCrudList) {
for (const key of formKeyMap[item.func]) {
feildValue[item.id + '-' + key] = (item as any)[key];
}
}
setFieldsValue(feildValue);
this.crudList = initialCrudList;
this.updateRender();
}
public getFormValidateData() {
let value = [] as IStringMap[];
const { crudList } = this;
this.props.form.validateFields((err: Error, values: any) => {
if (!err) {
let strategyList = [];
for (const item of crudList) {
const lineValue = {} as IStringMap;
const paramsArray = [] as number[];
// 不在commonKeys里的塞到params
for (const key of Object.keys(values)) {
if (key.indexOf(item.id) > -1) {
const finalKey = key.substring(key.indexOf('-') + 1);
if (commonKeys.indexOf(finalKey) < 0) { // 不在commonKeys里的塞到params 奇奇怪怪的接口
paramsArray.push(finalKey === 'day' ? values[key] * 24 * 60 * 60 : values[key]); // 按接口单位天的时候需要换算成秒
} else { // 在commonKeys里直接赋值
lineValue[finalKey] = values[key];
}
}
}
if (lineValue.func === 'happen' && paramsArray.length > 1 && paramsArray[0] < paramsArray[1]) {
strategyList = []; // 清空赋值
return message.error('周期值应大于次数') ;
}
lineValue.params = paramsArray.join(',');
strategyList.push(lineValue);
}
value = strategyList;
}
});
return value;
}
public remove = (curr: string) => {
const { crudList } = this;
if (crudList.length <= 1) {
return message.info('至少保留一项');
}
const index = crudList.findIndex(item => item.id === curr);
crudList.splice(index, 1);
this.updateRender();
}
public add = () => {
const { maxLimit = 5 } = this.props;
const { crudList } = this;
if (crudList.length >= maxLimit) {
return message.info('已达最大数量');
}
const id = `${crudList.length}_`;
crudList.push({
id,
func: 'happen',
});
this.updateRender();
}
public onFuncTypeChange = (e: string, key: string) => {
const { crudList } = this;
const index = crudList.findIndex(row => row.id === key);
if (index > -1) {
crudList[index].func = e;
}
this.updateRender();
}
public getFormItem(item: IFormItem) {
switch (item.type) {
default:
case FormItemType.input:
return <InputNumber min={0} key={item.key} {...item.attrs} disabled={this.isDetailPage} />;
case FormItemType.select:
return (
<Select
key={item.key}
{...item.attrs}
disabled={this.isDetailPage}
{...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 > 15 ? <Tooltip placement="bottomLeft" title={v.label}>
{v.label}
</Tooltip> : v.label}
</Select.Option>
))}
</Select>
);
}
}
public getFuncItem(row: ICRUDItem) {
const key = row.id;
const funcType = row.func;
let element = null;
const common = (
<>
{this.renderFormItem({ type: 'input', key: key + '-period', defaultValue: row.period } as IFormItem)}
</>
);
const equalItem = {
type: 'select',
attrs: { className: 'small-size' },
defaultValue: row.eopt || '=',
options: equalList,
key: key + '-eopt',
} as IFormSelect;
switch (funcType) {
case 'happen':
case 'ndiff':
element = (
<>
{common}
{this.renderFormItem({ type: 'input', key: key + '-count', defaultValue: row.count } as IFormItem)}
{this.renderFormItem(equalItem)}
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
</>
);
break;
case 'all':
case 'diff':
case 'max':
case 'min':
case 'sum':
case 'avg':
element = (
<>
{common}
{this.renderFormItem(equalItem)}
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
</>
);
break;
case 'c_avg_rate_abs':
case 'c_avg_rate':
element = (
<>
{common}
{this.renderFormItem({ type: 'input', key: key + '-day', defaultValue: row.day } as IFormItem)}
{this.renderFormItem(equalItem)}
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
%
</>
);
break;
case 'c_avg_abs':
case 'c_avg':
element = (
<>
{common}
{this.renderFormItem({ type: 'input', key: key + '-day', defaultValue: row.day } as IFormItem)}
{this.renderFormItem(equalItem)}
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
</>
);
break;
case 'pdiff':
element = (
<>
{common}
{this.renderFormItem(equalItem)}
{this.renderFormItem({ type: 'input', key: key + '-threshold', defaultValue: row.threshold } as IFormItem)}
%
</>
);
break;
}
return element;
}
public renderFormList(row: ICRUDItem) {
const key = row.id;
const funcType = row.func;
return (
<div key={key} className="form-list">
{this.renderFormItem({
type: 'select',
defaultValue: funcType,
attrs: {
onChange: (e: string) => this.onFuncTypeChange(e, key),
},
options: funcList,
key: key + '-func',
} as IFormSelect)}
{this.getFuncItem(row)}
</div>
);
}
public renderFormItem(item: IFormItem) {
const { getFieldDecorator } = this.props.form;
const initialValue = item.defaultValue || '';
const getFieldValue = {
initialValue,
rules: item.rules || [{ required: true, message: '请填写' }],
};
return (
<Form.Item
key={item.key}
>
{getFieldDecorator(item.key, getFieldValue)(
this.getFormItem(item),
)}
</Form.Item>
);
}
public render() {
const { crudList } = this;
const { maxLimit = 5 } = this.props;
return (
<Form>
{crudList.map((row, index) => {
return (
<div key={index}>
{this.renderFormList(row)}
{
crudList.length > 1 ? (
<Icon
className={this.isDetailPage ? 'is-show' : 'dynamic-button delete'}
type="minus-circle-o"
onClick={() => this.remove(row.id)}
/>
) : null
}
{index === crudList.length - 1 && crudList.length < maxLimit ? (
<Icon
className={this.isDetailPage ? 'is-show' : 'dynamic-button plus'}
type="plus-circle-o"
onClick={() => this.add()}
/>
) : null}
</div>
);
})}
</Form>
);
}
}
export const WrappedDynamicSetStrategy = Form.create({ name: 'dynamic_form_item' })(DynamicSetStrategy);

View File

@@ -0,0 +1,137 @@
import { getAlarmTime, getAlarmWeek } from './config';
import * as React from 'react';
import { IRequestParams, IAlarmTime } from 'types/alarm';
import { Checkbox, TimePicker, Form } from 'component/antd';
import { weekOptions } from 'constants/status-map';
import moment = require('moment');
interface ITimeProps {
form?: any;
formData?: any;
}
export class TimeForm extends React.Component<ITimeProps> {
public isDetailPage = window.location.pathname.includes('/alarm-detail'); // 判断是否为详情
public $form: any = null;
public weeks: number[] = [0, 1, 2, 3, 4, 5, 6, 7];
public startTime: number = 0;
public endTime: number = 23;
public getFormData() {
let value = null as IAlarmTime;
this.props.form.validateFields((error: Error, result: any) => {
if (error) {
return;
}
const start = Number(moment(result.startTime).format('HH'));
const end = Number(moment(result.endTime).format('HH'));
const timeArr = getAlarmTime().defaultTime;
const hours = timeArr.slice(start, end + 1);
value = {
weeks: result.weeks,
hours,
};
});
return value;
}
public resetFormData() {
const { defaultTime } = getAlarmTime();
const { defWeek } = getAlarmWeek();
this.props.form.setFieldsValue({
hours: defaultTime,
weeks: defWeek,
startTime: moment(0, 'HH'),
endTime: moment(23, 'HH'),
});
}
public updateFormData = (monitorRule: IRequestParams) => {
const selectHours = monitorRule.periodHoursOfDay.split(',').map(item => +item);
const selectWeek = monitorRule.periodDaysOfWeek.split(',').map(item => +item);
this.props.form.setFieldsValue({
// hours: selectHours,
weeks: selectWeek,
startTime: moment(selectHours[0], 'HH'),
endTime: moment(selectHours[selectHours.length - 1], 'HH'),
});
this.startTime = selectHours[0];
this.endTime = selectHours[selectHours.length - 1];
}
public onStartChange = (time: any, timeString: string) => {
this.startTime = Number(timeString);
}
public disabledHours = () => {
const hours = [] as number[];
for (let i = 0; i < this.startTime; i++) {
hours.push(i);
}
return hours;
}
public render() {
// const formData = {};
// {/* <div className="alarm-x-form">
// <XFormComponent
// ref={form => this.$form = form}
// formData={formData}
// formMap={xTimeFormMap}
// formLayout={formLayout}
// />
// </div> */}
const { getFieldDecorator } = this.props.form;
const format = 'HH';
return (
<div className="config-wrapper">
<span className="span-tag"></span>
<div className="alarm-time-form">
<Form name="basic" >
<b></b>
<Form.Item label="" key={1} className="form-item">
{getFieldDecorator('weeks', {
initialValue: this.weeks,
rules: [{ required: true, message: '请选择周期' }],
})(
<Checkbox.Group
options={weekOptions}
disabled={this.isDetailPage}
/>)}
</Form.Item>
<b></b>
<Form.Item label="" key={2} className="form-item">
{getFieldDecorator('startTime', {
initialValue: moment(this.startTime, format),
rules: [{ required: true, message: '请选择开始时间' }],
})(
<TimePicker
key={1}
format={format}
style={{width: 60}}
onChange={this.onStartChange}
disabled={this.isDetailPage}
/>)}
</Form.Item>
<b>~</b>
<Form.Item label="" key={3} className="form-item">
{getFieldDecorator('endTime', {
initialValue: moment(this.endTime, format),
rules: [{ required: true, message: '请选择结束时间' }],
})(
<TimePicker
key={2}
format={format}
disabledHours={this.disabledHours}
style={{width: 60}}
disabled={this.isDetailPage}
/>)}
</Form.Item>
</Form>
</div>
</div>
);
}
}
export const WrappedTimeForm = Form.create({ name: 'dynamic_time_form' })(TimeForm);

View File

@@ -0,0 +1,74 @@
import { XFormComponent } from 'component/x-form';
import { xTypeFormMap } from './config';
import * as React from 'react';
import { IRequestParams, ITypeForm } from 'types/alarm';
import { app } from 'store/app';
import { observer } from 'mobx-react';
import { WrappedDynamicSetFilter } from './filter-form';
@observer
export class TypeForm extends React.Component {
public $form: any = null;
public filterForm: any = null;
public getFormData() {
const filterObj = this.filterForm.getFormValidateData();
let typeValue = null as ITypeForm;
this.$form.validateFields((error: Error, result: ITypeForm) => {
if (error) {
return;
}
typeValue = result;
});
const valueObj = {
typeValue,
filterObj,
};
return valueObj;
}
public resetFormData() {
this.$form.resetFields();
this.filterForm.resetForm();
}
public updateFormData(monitorRule: IRequestParams) {
this.$form.setFieldsValue({
app: monitorRule.appId,
alarmName: monitorRule.name,
});
this.filterForm.initFormValue(monitorRule);
}
public render() {
const formData = {};
xTypeFormMap[1].options = app.data.map(item => ({
label: item.name,
value: item.appId,
}));
return (
<>
<div className="config-wrapper">
<span className="span-tag"></span>
<div className="alarm-x-form type-form">
<XFormComponent
ref={form => this.$form = form}
formData={formData}
formMap={xTypeFormMap}
layout="inline"
/>
</div>
</div >
<div className="config-wrapper">
<span className="span-tag"></span>
<div className="alarm-x-form type-form">
<WrappedDynamicSetFilter wrappedComponentRef={(form: any) => this.filterForm = form} />
</div>
</div >
</>
);
}
}

View File

@@ -0,0 +1,103 @@
import * as React from 'react';
import { Table, Button } from 'component/antd';
import { urlPrefix } from 'constants/left-menu';
import moment from 'moment';
import { alarm } from 'store/alarm';
import Url from 'lib/url-parser';
import { SearchAndFilterContainer } from 'container/search-filter';
import { IMonitorAlerts } from 'types/base-type';
import './index.less';
import { observer } from 'mobx-react';
import { timeFormat } from 'constants/strategy';
@observer
export class AlarmHistory extends SearchAndFilterContainer {
public id: number = null;
public startTime: any = moment().subtract(3, 'day').format('x');
public endTime: any = moment().endOf('day').format('x');
public state = {
filterStatus: false,
};
constructor(props: any) {
super(props);
const url = Url();
this.id = Number(url.search.id);
}
public historyCreateTime(value?: number) {
this.startTime = value ? moment().subtract(7, 'day').format('x') : moment().subtract(3, 'day').format('x');
this.endTime = moment().format('x');
alarm.getMonitorAlerts(this.id, this.startTime, this.endTime);
}
public historySelect() {
return(
<>
<div className="alarm-history-day">
<Button onClick={() => this.historyCreateTime()}></Button>
<Button onClick={() => this.historyCreateTime(7)}></Button>
</div>
</>
);
}
public historyTable() {
const monitorAlerts: IMonitorAlerts[] = alarm.monitorAlerts ? alarm.monitorAlerts : [];
const alertStatus = Object.assign({
title: '状态',
dataIndex: 'alertStatus',
key: 'alertStatus',
filters: [{ text: '故障', value: '0' }, { text: '已恢复', value: '1' }],
onFilter: (value: string, record: IMonitorAlerts) => record.alertStatus === Number(value),
render: (t: number) => t === 0 ? '故障' : '已恢复',
}, this.renderColumnsFilter('filterStatus'));
const columns = [
{
title: '监控名称',
dataIndex: 'monitorName',
key: 'monitorName',
render: (text: string, record: IMonitorAlerts) => (
<a href={`${urlPrefix}/alarm/history-detail?alertId=${record.alertId}`}> {text} </a>),
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
render: (time: number) => moment(time).format(timeFormat),
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
render: (time: number) => moment(time).format(timeFormat),
},
alertStatus,
{
title: '监控级别',
dataIndex: 'monitorPriority',
key: 'monitorPriority',
},
];
return (
<>
<Table rowKey="key" dataSource={monitorAlerts} columns={columns} loading={alarm.loading}/>
</>
);
}
public componentDidMount() {
alarm.getMonitorAlerts(this.id, this.startTime, this.endTime);
}
public render() {
return(
<>
{this.historySelect()}
{this.historyTable()}
</>
);
}
}

View File

@@ -0,0 +1,141 @@
import * as React from 'react';
import { createMonitorSilences } from 'container/modal';
import { IMonitorAlert, IMonitorMetric } from 'types/base-type';
import { Divider, Table, Button, PageHeader, Spin, Tooltip } from 'component/antd';
import { alarm } from 'store/alarm';
import { observer } from 'mobx-react';
import { handlePageBack } from 'lib/utils';
import LineChart, { hasData } from 'component/chart/line-chart';
import { EChartOption } from 'echarts';
import { timeFormat } from 'constants/strategy';
import Url from 'lib/url-parser';
import moment = require('moment');
import './index.less';
@observer
export class HistoryDetail extends React.Component {
public alertId: number;
constructor(props: any) {
super(props);
const url = Url();
this.alertId = Number(url.search.alertId);
}
public componentDidMount() {
alarm.getAlertsDetail(this.alertId);
}
public getChartOption = () => {
return alarm.getMetircHistoryChartOptions();
}
public renderNoData = (height?: number) => {
const style = { height: `${height}px`, lineHeight: `${height}px` };
return <div className="no-data-info" style={{ ...style }} key="noData"></div>;
}
public renderLoading = (height?: number) => {
const style = { height: `${height}px`, lineHeight: `${height}px` };
return <div className="no-data-info" style={{ ...style }} key="loading"><Spin /></div>;
}
public renderEchart = (options: EChartOption, loading = false) => {
const data = hasData(options);
if (loading) return this.renderLoading(400);
if (!data) return this.renderNoData(400);
return (
<div className="chart">
<LineChart height={400} options={options} key="chart" />
</div>);
}
public renderHistoricalTraffic(metric: IMonitorMetric) {
const option = this.getChartOption() as EChartOption;
return (
<>
<div className="history-left">
<div className="chart-box-0">
<div className="chart-title metric-head">
<span>{metric.metric}</span>
</div>
<Divider />
{this.renderEchart(option)}
</div>
</div>
</>
);
}
public renderAlarmEventDetails(alert: IMonitorAlert) {
const pointsColumns = [
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
render: (t: number) => moment(t * 1000).format(timeFormat),
},
{
title: 'value',
dataIndex: 'value',
key: 'value',
}];
return (
<>
<div className="history-right">
<div className="history-right-header">
<h2></h2>
<Button onClick={() => { createMonitorSilences(alert.monitorId, alert.monitorName); }}></Button>
</div>
<Divider className="history-right-divider" />
<ul>
<li><b></b>{alert.monitorName}</li>
<li><b></b>{alert.alertStatus === 0 ? '故障' : '已恢复'}</li>
<li><b></b>{alert.groups ? alert.groups.join('、') : null}</li>
<li><b></b>{alert.metric}</li>
<li><b></b>{moment(alert.startTime).format(timeFormat)}</li>
<li><b></b>{moment(alert.endTime).format(timeFormat)}</li>
<li><b></b>{alert.monitorPriority}</li>
<li><b></b>{alert.value}</li>
<li>
<b></b>
<Tooltip placement="bottomLeft" title={alert.info} >
{alert.info}
</Tooltip>
</li>
</ul>
<h4></h4>
<Table
rowKey="timestamp"
dataSource={alert.points}
columns={pointsColumns}
showHeader={false}
pagination={false}
bordered={true}
scroll={{ y: 260 }}
/>
</div>
</>
);
}
public render() {
return (
<>
{alarm.alertsDetail &&
<>
<PageHeader
className="detail topic-detail-header"
onBack={() => handlePageBack('/alarm')}
title={`${alarm.monitorAlert.monitorName || ''}`}
/>
<div className="alarm-history">
{this.renderHistoricalTraffic(alarm.monitorMetric)}
{this.renderAlarmEventDetails(alarm.monitorAlert)}
</div>
</>}
</>
);
}
}

View File

@@ -0,0 +1,64 @@
.alarm-history{
display: flex;
justify-content: space-around;
.history-left{
width: 60%;
}
.history-right{
width: 30%;
padding: 15px 20px;
background: #fff;
.history-right-divider{
margin-top: -5px;
margin-bottom: 10px;
}
.history-right-header{
display: flex;
justify-content: space-between;
h2{
font-size: 14px;
line-height: 38px;
}
}
ul{
li{
width: 100%;
font-size: 12px;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis; //超出部分以省略号显示
white-space: nowrap;
b{
font-size: 13px;
font-weight: 500;
}
}
}
}
}
.alarm-history-day{
width: 160px;
margin-bottom: 10px;
display: flex;
justify-content: space-around;
}
.monitor-detail{
font-size: 13px;
line-height: 24px;
}
.metric-head{
display: flex;
justify-content: space-between;
padding: 0 10px;
span{
font-size: 13px;
}
a{
margin: 0 5px;
font-size: 12px;
line-height: 24px;
}
}

View File

@@ -0,0 +1,64 @@
import * as React from 'react';
import { Tabs, PageHeader, Button } from 'antd';
import { observer } from 'mobx-react';
import { AlarmHistory } from './alarm-history';
import { handleTabKey } from 'lib/utils';
import { ShieldHistory } from './shield-history';
import { IXFormWrapper } from 'types/base-type';
import { alarm } from 'store/alarm';
import { IMonitorStrategyDetail } from 'types/alarm';
import { urlPrefix } from 'constants/left-menu';
import { createMonitorSilences } from 'container/modal';
import { AddAlarm } from '../add-alarm';
import { handlePageBack } from 'lib/utils';
import Url from 'lib/url-parser';
const { TabPane } = Tabs;
@observer
export class AlarmDetail extends React.Component {
public id: number = null;
public monitorName: any = null;
constructor(props: any) {
super(props);
const url = Url();
this.id = Number(url.search.id);
}
public render() {
let baseInfo = {} as IMonitorStrategyDetail;
if (alarm.monitorStrategyDetail) {
baseInfo = alarm.monitorStrategyDetail;
}
this.monitorName = baseInfo.name;
return(
<>
<PageHeader
className="detail topic-detail-header"
onBack={() => handlePageBack('/alarm')}
title={`${baseInfo.name || ''}`}
extra={[
<Button key="1" type="primary">
<a href={`${urlPrefix}/alarm/modify?id=${this.id}`}></a>
</Button>,
<Button onClick={() => {createMonitorSilences(this.id, this.monitorName); }} key="2" >
</Button>,
]}
/>
<Tabs activeKey={location.hash.substr(1) || '1'} type="card" onChange={handleTabKey}>
<TabPane tab="基本信息" key="1">
<AddAlarm />
</TabPane>
<TabPane tab="告警历史" key="2">
<AlarmHistory />
</TabPane>
<TabPane tab="屏蔽历史" key="3">
<ShieldHistory />
</TabPane>
</Tabs>
</>
);
}
}

View File

@@ -0,0 +1,180 @@
import * as React from 'react';
import { Table, notification, Modal, Popconfirm } from 'component/antd';
import moment from 'moment';
import { alarm } from 'store/alarm';
import { wrapper } from 'store';
import Url from 'lib/url-parser';
import { IMonitorSilences, IXFormWrapper } from 'types/base-type';
import { observer } from 'mobx-react';
import './index.less';
import { timeFormat } from 'constants/strategy';
@observer
export class ShieldHistory extends React.Component {
public id: number = null;
private xFormWrapper: IXFormWrapper;
constructor(props: any) {
super(props);
const url = Url();
this.id = Number(url.search.id);
}
public silencesDetail(record: IMonitorSilences) {
alarm.getSilencesDetail(record.silenceId).then((data) => {
if (alarm.silencesDetail) {
this.modifyInfo(alarm.silencesDetail);
}
});
}
public modifyInfo(record: IMonitorSilences) {
Modal.info({
title: '详情',
content: (
<ul className="monitor-detail">
<li><b></b>{record.monitorName}</li>
<li><b></b>{moment(record.startTime).format(timeFormat)}</li>
<li><b></b>{moment(record.endTime).format(timeFormat)}</li>
<li><b></b>{record.description}</li>
</ul>
),
});
}
public modifyMonitor(record: IMonitorSilences) {
this.xFormWrapper = {
formMap: [
{
key: 'monitorName',
label: '告警名称',
rules: [{
required: true,
message: '请输入告警名称',
}],
attrs: {
disabled: true,
},
},
{
key: 'beginEndTime',
label: '开始~结束时间',
type: 'range_picker',
rules: [{
required: true,
message: '请输入开始~结束时间',
}],
attrs: {
placeholder: ['开始时间', '结束时间'],
format: timeFormat,
showTime: true,
disabled: false,
ranges: {
'1小时': [moment(), moment().add(1, 'hour')],
'2小时': [moment(), moment().add(2, 'hour')],
'6小时': [moment(), moment().add(6, 'hour')],
'12小时': [moment(), moment().add(12, 'hour')],
'1天': [moment(), moment().add(1, 'day')],
'2天': [moment(), moment().add(7, 'day')],
'7天': [moment(), moment().add(7, 'day')],
},
},
},
{
key: 'description',
label: '说明',
type: 'text_area',
rules: [{
required: true,
}],
attrs: {
disabled: false,
placeholder: '请输入备注',
},
},
],
formData: {
monitorName: record.monitorName,
beginEndTime: [moment(record.startTime), moment(record.endTime)],
description: record.description,
},
okText: '确认',
visible: true,
width: 600,
title: '编辑',
onSubmit: (value: any) => {
const params = {
description: value.description,
startTime: +moment(value.beginEndTime[0]).format('x'),
endTime: +moment(value.beginEndTime[1]).format('x'),
id: record.silenceId,
monitorId: record.monitorId,
} as IMonitorSilences;
alarm.modifyMask(params, this.id).then(data => {
notification.success({ message: '修改成功' });
});
},
};
wrapper.open(this.xFormWrapper);
}
public deleteSilences(record: IMonitorSilences) {
alarm.deleteSilences(this.id, record.silenceId).then(data => {
notification.success({ message: '删除成功' });
});
}
public componentDidMount() {
alarm.getMonitorSilences(this.id);
}
public render() {
const monitorSilences: IMonitorSilences[] = alarm.monitorSilences ? alarm.monitorSilences : [];
const monitorColumns = [
{
title: '监控名称',
dataIndex: 'monitorName',
key: 'monitorName',
render: (text: string) => <span>{text}</span>,
}, {
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
render: (t: number) => moment(t).format(timeFormat),
}, {
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
render: (t: number) => moment(t).format(timeFormat),
}, {
title: '备注',
dataIndex: 'description',
key: 'description',
}, {
title: '操作',
dataIndex: 'option',
key: 'option',
render: (action: any, record: IMonitorSilences) => {
return(
<>
<a onClick={() => this.modifyMonitor(record)} className="action-button"></a>
<a onClick={() => this.silencesDetail(record)} className="action-button"></a>
<Popconfirm
title="确定删除?"
onConfirm={() => this.deleteSilences(record)}
>
<a></a>
</Popconfirm>
</>
);
},
},
];
return(
<>
<Table dataSource={monitorSilences} columns={monitorColumns} />
</>
);
}
}

View File

@@ -0,0 +1,87 @@
import * as React from 'react';
import { Table, Button } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { observer } from 'mobx-react';
import { app } from 'store/app';
import { getAlarmColumns } from './add-alarm/config';
import { IMonitorStrategies } from 'types/base-type';
import { pagination } from 'constants/table';
import { urlPrefix } from 'constants/left-menu';
import { alarm } from 'store/alarm';
import 'styles/table-filter.less';
@observer
export class AlarmList extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
public getData<T extends IMonitorStrategies>(origin: T[]) {
let data: T[] = [];
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
if (app.active !== '-1' || searchKey !== '') {
data = origin.filter(d =>
((d.name !== undefined && d.name !== null) && d.name.toLowerCase().includes(searchKey as string)
|| ((d.operator !== undefined && d.operator !== null) && d.operator.toLowerCase().includes(searchKey as string)))
&& (app.active === '-1' || d.appId === (app.active + '')),
);
} else {
data = origin;
}
return data;
}
public renderTableList(data: IMonitorStrategies[]) {
return (
<Table
rowKey="key"
columns={getAlarmColumns(urlPrefix)}
dataSource={data}
pagination={pagination}
/>
);
}
public renderTable() {
return this.renderTableList(this.getData(alarm.monitorStrategies));
}
public renderOperationPanel() {
return (
<>
{this.renderApp('应用:')}
{this.renderSearch('名称:', '请输入告警名称或者操作人')}
<li className="right-btn-1">
<Button type="primary">
<a href={`${urlPrefix}/alarm/add`}>
</a>
</Button>
</li>
</>
);
}
public componentDidMount() {
if (!alarm.monitorStrategies.length) {
alarm.getMonitorStrategies();
}
}
public render() {
return (
<div className="container">
<div className="table-operation-panel">
<ul>
{this.renderOperationPanel()}
</ul>
</div>
<div className="table-wrapper">
{this.renderTable()}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,2 @@
export * from './alarm-list';
export * from './add-alarm';

View File

@@ -0,0 +1,46 @@
import { Select, Tooltip } from 'component/antd';
import { urlPrefix } from 'constants/left-menu';
import { searchProps } from 'constants/table';
import * as React from 'react';
const Option = Select.Option;
interface IStaffSelectProps {
selectData?: any[];
onChange?: (result: string []) => any;
value?: string[];
}
export class AppSelect extends React.Component<IStaffSelectProps> {
public render() {
const { value, selectData } = this.props;
const query = `application=1`;
return (
<>
<Select
placeholder="请选择"
value={value || []}
onChange={(e: string []) => this.handleChange(e)}
{...searchProps}
>
{selectData.map((d: any) =>
<Option value={d.appId} key={d.appId}>
{d.name.length > 25 ? <Tooltip placement="bottomLeft" title={d.name}>{d.name}</Tooltip> : d.name}
</Option>)}
</Select>
{
selectData.length ? null : <i>
<a href={`${urlPrefix}/topic/app-list?${query}`}></a>
</i>}
</>
);
}
public handleChange(params: string []) {
const { onChange } = this.props;
// tslint:disable-next-line:no-unused-expression
onChange && onChange(params);
}
}

View File

@@ -0,0 +1,205 @@
import * as React from 'react';
import { Table, Tabs, PageHeader, Descriptions, Divider, Spin, Icon, Tooltip } from 'component/antd';
import { ILabelValue, ITopic, IAppItem, IConnectionInfo } from 'types/base-type';
import urlQuery from 'store/url-query';
import { tableFilter } from 'lib/utils';
import { app } from 'store/app';
import { topicStatusMap } from 'constants/status-map';
import { urlPrefix } from 'constants/left-menu';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import { copyString } from 'lib/utils';
import { region } from 'store/region';
import { timeFormat } from 'constants/strategy';
import { modal } from 'store/modal';
import { SearchAndFilterContainer } from 'container/search-filter';
import { handlePageBack } from 'lib/utils';
import moment = require('moment');
import './index.less';
const { TabPane } = Tabs;
@observer
export class AppDetail extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
public getColumns(data: ITopic[]) {
const statusColumn = Object.assign({
title: '权限',
dataIndex: 'access',
key: 'access',
filters: tableFilter<ITopic>(data, 'access', topicStatusMap),
onFilter: (text: number, record: ITopic) => record.access === text,
render: (val: number) => (
<div className={val === 0 ? '' : 'success'}>
{topicStatusMap[val] || ''}
</div>
),
}, this.renderColumnsFilter('filterStatus')) as any;
const { currentTab } = app;
const columns = [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
sorter: (a: ITopic, b: ITopic) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (text: string, r: ITopic) => (
<Tooltip placement="bottomLeft" title={text}>
<a
// tslint:disable-next-line:max-line-length
href={`${urlPrefix}/topic/topic-detail?clusterId=${r.clusterId}&topic=${r.topicName}&region=${region.currentRegion}`}
>{text}
</a>
</Tooltip>),
}, {
title: '集群名称',
dataIndex: 'clusterName',
key: 'clusterName',
}, {
title: '申请时间',
dataIndex: 'gmtCreate',
key: 'gmtCreate',
render: (t: number) => moment(t).format(timeFormat),
},
statusColumn,
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
render: (text: string, record: ITopic) =>
<a key={record.key} onClick={() => this.cancelPermission(record)}></a>,
},
];
const tableColumns = [].concat(columns);
if (currentTab === '1') {
tableColumns.splice(4, 2);
}
return tableColumns;
}
public cancelPermission(record: ITopic) {
modal.showCancelTopicPermission(record);
}
public componentDidMount() {
if (urlQuery.appId) {
app.getAppDetail(urlQuery.appId);
app.getAppTopicList(urlQuery.appId);
}
}
public renderBaseInfo(baseInfo: IAppItem) {
const infoList: ILabelValue[] = [{
label: '应用名称',
value: baseInfo.name,
}, {
label: '负责人',
value: baseInfo.principals,
}];
const infoCopy: ILabelValue[] = [{
label: 'AppID',
value: baseInfo.appId,
}, {
label: '密钥',
value: baseInfo.password,
}];
return (
<PageHeader
onBack={() => handlePageBack('/topic/app-list')}
title={baseInfo.name || ''}
>
<Divider />
<Descriptions column={2}>
{infoList.map((item, key) => (
<Descriptions.Item key={key} label={item.label}>
<Tooltip placement="bottomLeft" title={item.value}>
<span className="overview-bootstrap">
<i className="overview-boot">{item.value}</i>
</span>
</Tooltip>
</Descriptions.Item>
))}
</Descriptions>
<Descriptions column={2}>
{infoCopy.map((item, key) => (
<Descriptions.Item key={key} label={item.label}>
<Icon
onClick={() => copyString(item.value)}
type="copy"
className="didi-theme"
/> {item.value}
</Descriptions.Item>
))}
</Descriptions>
<Descriptions size="small" column={1}>
<Descriptions.Item label="应用描述">
<Tooltip placement="bottomLeft" title={baseInfo.description}>
<span className="overview-bootstrap" style={{width: '600px'}}>
<i className="overview-boot"> {baseInfo.description} </i>
</span>
</Tooltip>
</Descriptions.Item>
</Descriptions>
</PageHeader>
);
}
public getData<T extends ITopic>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: ITopic) =>
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|| (item.clusterName !== undefined && item.clusterName !== null) && item.clusterName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderTable() {
return (
<Spin spinning={app.loading}>
<Table
columns={this.getColumns(app.topicList)}
dataSource={this.getData(app.topicList)}
pagination={pagination}
/>
</Spin>
);
}
public onChangeTab(e: string) {
app.setCurrentTab(e);
if (urlQuery.appId) {
app.getAppTopicList(urlQuery.appId);
}
}
public render() {
const { currentTab } = app;
return (
<>
<div className="app-container">
<div className="base-info">
{this.renderBaseInfo(app.baseInfo)}
</div>
<div className="k-row">
<Tabs defaultActiveKey="1" type="card" onChange={(e) => this.onChangeTab(e)}>
<TabPane tab="创建的Topic" key="1" />
<TabPane tab="有权限Topic" key="2" />
</Tabs>
<ul className="k-tab">
<li>{currentTab === '1' ? '创建的Topic' : '有权限Topic'}</li>
{this.renderSearch('', '请输入Topic名称/集群名称')}
</ul>
{this.renderTable()}
</div>
</div>
</>
);
}
}

View File

@@ -0,0 +1,136 @@
import { Table, Tooltip, Spin } from 'component/antd';
import { SearchAndFilterContainer } from 'container/search-filter';
import { observer } from 'mobx-react';
import 'styles/table-filter.less';
import { IAppItem } from 'types/base-type';
import { app } from 'store/app';
import { pagination, cellStyle } from 'constants/table';
import { showEditModal } from 'container/modal';
import { modal } from 'store/modal';
import * as React from 'react';
interface IProps {
from: string;
}
@observer
export class CommonAppList extends SearchAndFilterContainer {
public state = {
searchKey: '',
};
public from = 'topic';
constructor(props: IProps) {
super(props);
this.from = props.from;
}
public getColumns = (data: IAppItem[]) => {
const columns = [
{
title: 'AppID',
dataIndex: 'appId',
key: 'appId',
width: '15%',
sorter: (a: IAppItem, b: IAppItem) => a.appId.localeCompare(b.appId),
render: (text: string, record: IAppItem) => {
return (
<a href={`${this.urlPrefix}/topic/app-detail?appId=${record.appId}`}>{text}</a>
);
},
},
{
title: '应用名称',
dataIndex: 'name',
key: 'name',
width: '20%',
onCell: () => ({
style: {
maxWidth: 150,
...cellStyle,
},
}),
render: (text: string, record: IAppItem) => {
return (
<Tooltip placement="bottomLeft" title={record.name}>{text}</Tooltip>);
},
},
{
title: '应用描述',
dataIndex: 'description',
key: 'description',
width: '25%',
onCell: () => ({
style: {
maxWidth: 150,
...cellStyle,
},
}),
render: (text: string, record: IAppItem) => {
return (
<Tooltip placement="bottomLeft" title={record.description} >{text}</Tooltip>);
},
}, {
title: '负责人',
dataIndex: 'principals',
key: 'principals',
width: '25%',
onCell: () => ({
style: {
maxWidth: 150,
...cellStyle,
},
}),
render: (text: string) => <Tooltip placement="bottomLeft" title={text} >{text}</Tooltip>,
},
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
width: '15%',
render: (text: any, record: IAppItem) => {
return (
<span className="table-operation">
<a onClick={() => showEditModal(record, this.from)}></a>
<a onClick={() => showEditModal(record, this.from, true)}></a>
<a onClick={() => this.getOnlineConnect(record)}>线</a>
</span>);
},
},
];
return columns;
}
public getOnlineConnect(record: IAppItem) {
modal.showOfflineAppModal(record.appId);
}
public getData<T extends IAppItem>(origin: T[]) {
let data: T[] = [];
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IAppItem) =>
((item.name !== undefined && item.name !== null) && item.name.toLowerCase().includes(searchKey as string)) ||
((item.principals !== undefined && item.principals !== null) && item.principals.toLowerCase().includes(searchKey as string)) ||
((item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string)) ) : origin;
return data;
}
public renderTableList(data: IAppItem[]) {
return (
<>
<Spin spinning={app.loading}>
<Table
rowKey="key"
columns={this.getColumns(data)}
dataSource={data}
pagination={pagination}
/>
</Spin>
</>
);
}
}

View File

@@ -0,0 +1,38 @@
.app-container {
.base-info {
background-color: white;
margin-bottom: 20px;
}
.ant-divider-horizontal {
margin: 0px 0px 24px 0px;
}
}
.app-detail-ul {
border: 1px solid #000;
overflow: hidden;
text-overflow: ellipsis; //超出部分以省略号显示
white-space: nowrap;
min-width: 600px;
}
.custom-content {
margin-bottom: 20px;
}
.overview {
width: 700px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.appitem-detail {
margin-top: 30px;
p {
font-size: 13px;
}
}

View File

@@ -0,0 +1,2 @@
export * from './app-list';
export * from './app-detail';

View File

@@ -0,0 +1,159 @@
import * as React from 'react';
import { Table } from 'component/antd';
import Url from 'lib/url-parser';
import { cluster } from 'store/cluster';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import { IBrokerData, IEnumsMap } from 'types/base-type';
import { admin } from 'store/admin';
import { SearchAndFilterContainer } from 'container/search-filter';
import { transBToMB } from 'lib/utils';
import moment from 'moment';
import './index.less';
import { timeFormat } from 'constants/strategy';
@observer
export class ClusterBroker extends SearchAndFilterContainer {
public clusterId: number;
public clusterName: string;
public state = {
filterPeakFlowVisible: false,
filterReplicatedVisible: false,
filterStatusVisible: false,
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
this.clusterName = decodeURI(url.search.clusterName);
}
public getData<T extends IBrokerData>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IBrokerData) =>
(item.brokerId !== undefined && item.brokerId !== null) && (item.brokerId + '').toLowerCase().includes(searchKey as string)
|| (item.host !== undefined && item.host !== null) && item.host.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderBrokerData() {
let peakFlow = [] as IEnumsMap[];
peakFlow = admin.peakFlowStatusList ? admin.peakFlowStatusList : peakFlow;
const peakFlowStatus = Object.assign({
title: '峰值状态',
dataIndex: 'peakFlowStatus',
key: 'peakFlowStatus',
filters: peakFlow.map(ele => ({ text: ele.message, value: ele.code + '' })),
onFilter: (value: string, record: IBrokerData) => record.peakFlowStatus === +value,
render: (value: number) => {
let messgae: string;
peakFlow.map(ele => {
if (ele.code === value) {
messgae = ele.message;
}
});
return(
<span>{messgae}</span>
);
},
}, this.renderColumnsFilter('filterPeakFlowVisible'));
const underReplicated = Object.assign({
title: '副本状态',
dataIndex: 'underReplicated',
key: 'underReplicated',
filters: [{ text: '同步', value: 'false' }, { text: '未同步', value: 'true' }],
onFilter: (value: string, record: IBrokerData) => record.underReplicated === (value === 'true') ? true : false,
render: (t: boolean) => <span className={t ? 'fail' : 'success'}>{t ? '未同步' : '同步'}</span>,
}, this.renderColumnsFilter('filterReplicatedVisible'));
const status = Object.assign({
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [{ text: '未使用', value: '-1' }, { text: '使用中', value: '0' }],
onFilter: (value: string, record: IBrokerData) => record.status === Number(value),
render: (t: number) => t === 0 ? '使用中' : '未使用',
}, this.renderColumnsFilter('filterStatusVisible'));
const columns = [
{
title: 'ID',
dataIndex: 'brokerId',
key: 'brokerId',
sorter: (a: IBrokerData, b: IBrokerData) => b.brokerId - a.brokerId,
render: (text: number, record: IBrokerData) => <span>{text}</span>,
},
{
title: '主机',
dataIndex: 'host',
key: 'host',
sorter: (a: any, b: any) => a.host.charCodeAt(0) - b.host.charCodeAt(0),
},
{
title: 'Port',
dataIndex: 'port',
key: 'port',
sorter: (a: IBrokerData, b: IBrokerData) => b.port - a.port,
},
{
title: 'JMX Port',
dataIndex: 'jmxPort',
key: 'jmxPort',
sorter: (a: IBrokerData, b: IBrokerData) => b.jmxPort - a.jmxPort,
},
{
title: '启动时间',
dataIndex: 'startTime',
key: 'startTime',
sorter: (a: IBrokerData, b: IBrokerData) => b.startTime - a.startTime,
render: (time: number) => moment(time).format(timeFormat),
},
{
title: 'Bytes InMB/s',
dataIndex: 'byteIn',
key: 'byteIn',
sorter: (a: IBrokerData, b: IBrokerData) => b.byteIn - a.byteIn,
render: (t: number) => transBToMB(t),
},
{
title: 'Bytes OutMB/s',
dataIndex: 'byteOut',
key: 'byteOut',
sorter: (a: IBrokerData, b: IBrokerData) => b.byteOut - a.byteOut,
render: (t: number) => transBToMB(t),
},
// peakFlowStatus,
underReplicated,
status,
];
return (
<Table dataSource={this.getData(cluster.clusterBroker)} columns={columns} pagination={pagination} loading={cluster.loading} />
);
}
public componentDidMount() {
cluster.getClusterDetailBroker(this.clusterId);
// admin.getBrokersMetadata(this.clusterId);
}
public render() {
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入ID或主机')}
</ul>
{this.renderBrokerData()}
</div>
</>
);
}
}

View File

@@ -0,0 +1,140 @@
import * as React from 'react';
import { PageHeader, Descriptions, Tooltip, Icon, Spin } from 'component/antd';
import { ILabelValue, IBasicInfo, IOptionType, IClusterReal } from 'types/base-type';
import { selectOptionMap } from 'constants/status-map';
import { observer } from 'mobx-react';
import { cluster } from 'store/cluster';
import { clusterTypeMap } from 'constants/status-map';
import { copyString } from 'lib/utils';
import Url from 'lib/url-parser';
import moment from 'moment';
import './index.less';
import { StatusGraghCom } from 'component/flow-table';
import { renderTrafficTable, NetWorkFlow } from 'container/network-flow';
import { timeFormat } from 'constants/strategy';
interface IOverview {
basicInfo: IBasicInfo;
}
@observer
export class ClusterOverview extends React.Component<IOverview> {
public clusterId: number;
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public clusterContent() {
const content = this.props.basicInfo as IBasicInfo;
const clusterContent = [{
value: content.clusterName,
label: '集群名称',
}, {
value: clusterTypeMap[content.mode],
label: '集群类型',
}, {
value: moment(content.gmtCreate).format(timeFormat),
label: '接入时间',
}, {
value: content.physicalClusterId,
label: '物理集群ID',
}];
const clusterInfo = [{
value: content.clusterVersion,
label: 'kafka版本',
}, {
value: content.bootstrapServers,
label: 'Bootstrap Severs',
}];
return (
<>
<div className="chart-title"></div>
<PageHeader className="detail" title="">
<Descriptions size="small" column={3}>
{clusterContent.map((item: ILabelValue, index: number) => (
<Descriptions.Item key={index} label={item.label} >
{item.value}
</Descriptions.Item>
))}
{clusterInfo.map((item: ILabelValue, index: number) => (
<Descriptions.Item key={index} label={item.label}>
<Tooltip placement="bottomLeft" title={item.value}>
<span className="overview-bootstrap">
<Icon
onClick={() => copyString(item.value)}
type="copy"
className="didi-theme overview-theme"
/>
<i className="overview-boot">{item.value}</i>
</span>
</Tooltip>
</Descriptions.Item>
))}
</Descriptions>
</PageHeader>
</>
);
}
public updateRealStatus = () => {
cluster.getClusterDetailRealTime(this.clusterId);
}
public onSelectChange(e: IOptionType) {
return cluster.changeType(e);
}
public getOptionApi = () => {
return cluster.getClusterDetailMetrice(this.clusterId);
}
public componentDidMount() {
cluster.getClusterBasicInfo(this.clusterId);
cluster.getClusterDetailRealTime(this.clusterId);
}
public renderHistoryTraffic() {
return (
<NetWorkFlow
key="1"
selectArr={selectOptionMap}
type={cluster.type}
selectChange={(value: IOptionType) => this.onSelectChange(value)}
getApi={() => this.getOptionApi()}
/>
);
}
public renderTrafficInfo = () => {
return (
<Spin spinning={cluster.realLoading}>
{renderTrafficTable(this.updateRealStatus, StatusGragh)}
</Spin>
);
}
public render() {
return (
<>
<div className="base-info">
{this.clusterContent()}
{this.renderTrafficInfo()}
{this.renderHistoryTraffic()}
</div>
</>
);
}
}
@observer
export class StatusGragh extends StatusGraghCom<IClusterReal> {
public getData = () => {
return cluster.clusterRealData;
}
public getLoading = () => {
return cluster.realLoading;
}
}

View File

@@ -0,0 +1,152 @@
import * as React from 'react';
import Url from 'lib/url-parser';
import { cluster } from 'store/cluster';
import { Table, Tooltip } from 'antd';
import { pagination, cellStyle } from 'constants/table';
import { observer } from 'mobx-react';
import { IClusterTopics } from 'types/base-type';
import { SearchAndFilterContainer } from 'container/search-filter';
import { urlPrefix } from 'constants/left-menu';
import { transMSecondToHour } from 'lib/utils';
import { region } from 'store/region';
import './index.less';
import moment = require('moment');
import { timeFormat } from 'constants/strategy';
@observer
export class ClusterTopic extends SearchAndFilterContainer {
public clusterId: number;
public clusterTopicsFrom: IClusterTopics;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getData<T extends IClusterTopics>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IClusterTopics) =>
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|| (item.appName !== undefined && item.appName !== null) && item.appName.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public clusterTopicList() {
const columns = [
{
title: 'Topic名称',
dataIndex: 'topicName',
key: 'topicName',
width: '20%',
sorter: (a: IClusterTopics, b: IClusterTopics) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (text: string, record: IClusterTopics) => {
return (
<Tooltip placement="bottomLeft" title={record.topicName} >
<a
// tslint:disable-next-line:max-line-length
href={`${urlPrefix}/topic/topic-detail?clusterId=${record.logicalClusterId}&topic=${record.topicName}&region=${region.currentRegion}`}
>
{text}
</a>
</Tooltip>);
},
},
{
title: 'QPS',
dataIndex: 'produceRequest',
key: 'produceRequest',
width: '10%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.produceRequest - a.produceRequest,
render: (t: number) => t === null ? '' : t.toFixed(2),
},
{
title: 'Bytes In(KB/s)',
dataIndex: 'byteIn',
key: 'byteIn',
width: '15%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.byteIn - a.byteIn,
render: (val: number) => val === null ? '' : (val / 1024).toFixed(2),
},
{
title: '所属应用',
dataIndex: 'appName',
key: 'appName',
width: '15%',
render: (val: string, record: IClusterTopics) => (
<Tooltip placement="bottomLeft" title={record.appId} >
{val}
</Tooltip>
),
},
{
title: '保存时间h',
dataIndex: 'retentionTime',
key: 'retentionTime',
width: '15%',
sorter: (a: IClusterTopics, b: IClusterTopics) => b.retentionTime - a.retentionTime,
render: (time: any) => transMSecondToHour(time),
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: '20%',
render: (t: number) => moment(t).format(timeFormat),
},
{
title: 'Topic说明',
dataIndex: 'description',
key: 'description',
width: '30%',
onCell: () => ({
style: {
maxWidth: 200,
...cellStyle,
},
}),
render: (val: string) => (
<Tooltip placement="topLeft" title={val} >
{val}
</Tooltip>
),
},
];
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Topic名称或所属应用')}
</ul>
<Table
loading={cluster.loading}
rowKey="topicName"
dataSource={this.getData(cluster.clusterTopics)}
columns={columns}
pagination={pagination}
/>
</div>
</>
);
}
public componentDidMount() {
cluster.getClusterDetailTopics(this.clusterId);
}
public render() {
return (
cluster.clusterTopics ? <> {this.clusterTopicList()} </> : null
);
}
}

View File

@@ -0,0 +1,102 @@
import * as React from 'react';
import { SearchAndFilterContainer } from 'container/search-filter';
import { Table, Tooltip } from 'component/antd';
import { observer } from 'mobx-react';
import { pagination } from 'constants/table';
import Url from 'lib/url-parser';
import { IThrottles } from 'types/base-type';
import { cluster } from 'store/cluster';
import './index.less';
@observer
export class CurrentLimiting extends SearchAndFilterContainer {
public clusterId: number;
public state = {
searchKey: '',
};
constructor(props: any) {
super(props);
const url = Url();
this.clusterId = Number(url.search.clusterId);
}
public getData<T extends IThrottles>(origin: T[]) {
let data: T[] = origin;
let { searchKey } = this.state;
searchKey = (searchKey + '').trim().toLowerCase();
data = searchKey ? origin.filter((item: IThrottles) =>
(item.topicName !== undefined && item.topicName !== null) && item.topicName.toLowerCase().includes(searchKey as string)
|| (item.appId !== undefined && item.appId !== null) && item.appId.toLowerCase().includes(searchKey as string),
) : origin ;
return data;
}
public renderController() {
const clientType = Object.assign({
title: '类型',
dataIndex: 'throttleClientType',
key: 'throttleClientType',
filters: [{ text: 'fetch', value: 'FetchThrottleTime' }, { text: 'produce', value: 'ProduceThrottleTime' }],
onFilter: (value: string, record: IThrottles) => record.throttleClientType === value,
render: (t: string) => t,
}, this.renderColumnsFilter('filterStatus'));
const columns = [
{
title: 'Topic名称',
key: 'topicName',
dataIndex: 'topicName',
sorter: (a: IThrottles, b: IThrottles) => a.topicName.charCodeAt(0) - b.topicName.charCodeAt(0),
render: (val: string) => <Tooltip placement="bottomLeft" title={val}> {val} </Tooltip>,
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
sorter: (a: IThrottles, b: IThrottles) => a.appId.charCodeAt(0) - b.appId.charCodeAt(0),
},
clientType,
{
title: 'Broker',
dataIndex: 'brokerIdList',
key: 'brokerIdList',
render: (value: number[]) => {
const num = value ? `[${value.join(',')}]` : '';
return(
<span>{num}</span>
);
},
},
];
return (
<Table
columns={columns}
dataSource={this.getData(cluster.clustersThrottles)}
pagination={pagination}
rowKey="key"
/>
);
}
public componentDidMount() {
cluster.getClusterDetailThrottles(this.clusterId);
}
public render() {
return (
<>
<div className="k-row">
<ul className="k-tab">
<li>{this.props.tab}</li>
{this.renderSearch('', '请输入Topic名称或AppId')}
</ul>
{this.renderController()}
</div>
</>
);
}
}

View File

@@ -0,0 +1,63 @@
.table-operation-bar {
position: absolute;
right: 24px;
z-index: 100;
li {
display: inline-block;
vertical-align: middle;
.ant-select {
width: 150px;
}
.ant-input-search {
width: 200px;
}
}
}
.traffic-table {
margin: 10px 0;
.traffic-header {
width: 100%;
height: 44px;
font-weight: bold;
background: rgb(245, 245, 245);
border: 1px solid #e8e8e8;
padding: 0 10px;
display: flex;
justify-content: space-between;
span {
color: rgba(0, 0, 0, 0.65);
font-size: 13px;
line-height: 44px;
font-weight: 100;
}
.k-abs {
font-size: 12px;
}
}
}
.implement-button {
float: right;
margin-right: -120px;
}
.overview-bootstrap {
width: 200px;
display: flex;
justify-content: start;
line-height: 20px;
.overview-theme {
margin: 3px;
}
.overview-boot {
font-style: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

Some files were not shown because too many files have changed in this diff Show More