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

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';