@@ -1 +1,6 @@ | |||
# AIStudio ad-hub-frontend | |||
# AIStudio apsc-frontend | |||
``` | |||
npm i // 安装依赖(用yarn会有问题,所以 yarn.lock 文件可能会有依赖缺失) | |||
npm/yarn start // 启动项目 | |||
``` |
@@ -7,8 +7,8 @@ | |||
*/ | |||
// const file_url = 'https://182.138.104.162:16480/'; | |||
// const server_url = 'https://182.138.104.162:16480/'; //162环境 | |||
const server_url = 'https://123.60.231.101/' //蓝区 | |||
// const server_url = 'https://182.138.104.162:16480/'; // 成都蓝区 | |||
const server_url = 'https://123.60.231.101/' // 深圳蓝区 | |||
export default { | |||
dev: { | |||
@@ -28,6 +28,14 @@ export default { | |||
}, | |||
secure: false, | |||
}, | |||
'/aom/api/v1': { | |||
target: server_url, | |||
changeOrigin: true, | |||
pathRewrite: { | |||
'^': '', | |||
}, | |||
secure: false, | |||
}, | |||
// '/file_server': { | |||
// target: file_url, | |||
// changeOrigin: true, | |||
@@ -25,8 +25,37 @@ export default [ | |||
// }, | |||
}, | |||
{ | |||
path: '/resource', | |||
name: 'resource', | |||
path: '/resourceManagement', | |||
name: 'resourceManagement', | |||
icon: 'ProfileOutlined', | |||
routes: [ | |||
{ | |||
path: '/resourceManagement/monitor', | |||
name: 'monitor', | |||
component: './ResourceManagement/Monitor', | |||
}, | |||
{ | |||
path: '/resourceManagement/alert/list', | |||
name: 'alert', | |||
component: './ResourceManagement/Alert/' | |||
}, | |||
{ | |||
path: '/resourceManagement/alert/create', | |||
name: 'createRule', | |||
component: './ResourceManagement/Alert/CreatRule', | |||
hideInMenu: true, | |||
}, | |||
{ | |||
path: '/resourceManagement/alert/edit/:id', | |||
name: 'editRule', | |||
component: './ResourceManagement/Alert/CreatRule', | |||
hideInMenu: true, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '/resourceAllocation', | |||
name: 'resourceAllocation', | |||
icon: 'ReconciliationOutlined', | |||
// authority: { | |||
// anyOf: [ | |||
@@ -46,9 +75,9 @@ export default [ | |||
// }, | |||
routes: [ | |||
{ | |||
path: '/resource/organization', | |||
path: '/resourceAllocation/organization', | |||
name: 'organization', | |||
component: './VirtualCluster/', | |||
component: './Organization/', | |||
// authority: { | |||
// anyOf: [ | |||
// [AOM_PREFIX + '/virtual-clusters/{id}', 'get'], | |||
@@ -61,9 +90,9 @@ export default [ | |||
// }, | |||
}, | |||
{ | |||
path: '/resource/organization/create', | |||
name: 'createVirtualCluster', | |||
component: './VirtualCluster/AddAndEdit', | |||
path: '/resourceAllocation/organization/create', | |||
name: 'createOrganization', | |||
component: './Organization/AddAndEdit', | |||
hideInMenu: true, | |||
// authority: { | |||
// anyOf: [ | |||
@@ -73,9 +102,9 @@ export default [ | |||
// }, | |||
}, | |||
{ | |||
path: '/resource/organization/edit/:id', | |||
name: 'editVirtualCluster', | |||
component: './VirtualCluster/AddAndEdit', | |||
path: '/resourceAllocation/organization/edit/:id', | |||
name: 'editOrganization', | |||
component: './Organization/AddAndEdit', | |||
hideInMenu: true, | |||
// authority: { | |||
// anyOf: [ | |||
@@ -85,9 +114,9 @@ export default [ | |||
// }, | |||
}, | |||
{ | |||
path: '/resource/userGroup', | |||
path: '/resourceAllocation/userGroup', | |||
name: 'userGroup', | |||
component: './ResourceManage/list', | |||
component: './UserGroup/list', | |||
// authority: { | |||
// anyOf: [ | |||
// [AOM_PREFIX + '/resource/create', 'post'], | |||
@@ -97,9 +126,9 @@ export default [ | |||
// }, | |||
}, | |||
{ | |||
path: '/resource/userGroup/:id', | |||
path: '/resourceAllocation/userGroup/:id', | |||
name: 'editUserGroup', | |||
component: './ResourceManage/create', | |||
component: './UserGroup/create', | |||
hideInMenu: true, | |||
// authoriry: { | |||
// allOf: [ | |||
@@ -110,9 +139,9 @@ export default [ | |||
// }, | |||
}, | |||
{ | |||
path: '/resource/userGroup/create', | |||
path: '/resourceAllocation/userGroup/create', | |||
name: 'createUserGroup', | |||
component: './ResourceManage/create', | |||
component: './UserGroup/create', | |||
hideInMenu: true, | |||
// authority: { | |||
// allOf: [ | |||
@@ -1216,9 +1216,9 @@ | |||
} | |||
}, | |||
"@apulis/table": { | |||
"version": "1.9.1", | |||
"resolved": "https://registry.npmjs.org/@apulis/table/-/table-1.9.1.tgz", | |||
"integrity": "sha512-BajA102zHXSIerY6JfpbOGy8hs7qAQCTayCiE3v5/cPpHH4O54I3LMEGm69QFdLQn2Wyeu56a1U3Ql+MjF6O7w==", | |||
"version": "1.9.3", | |||
"resolved": "https://registry.npmjs.org/@apulis/table/-/table-1.9.3.tgz", | |||
"integrity": "sha512-SjHMNCc3JFu/8hlHSjiZ8IMbi+HDkFdVMPz38N5FClnpr5wfzkLJLK18nw5HG8z5gko7sniyPBDZZk8Co4Vrlg==", | |||
"requires": { | |||
"@ant-design/pro-form": "^1.32.2", | |||
"@apulis/provider": "0.8.0", | |||
@@ -63,7 +63,7 @@ | |||
"@apulis/header": "^0.1.3", | |||
"@apulis/provider": "^0.6.0", | |||
"@apulis/request": "^0.2.1", | |||
"@apulis/table": "^1.9.1", | |||
"@apulis/table": "^1.9.3", | |||
"@types/lodash.debounce": "^4.0.6", | |||
"@types/lodash.isequal": "^4.5.5", | |||
"@types/uuid": "^8.3.1", | |||
@@ -15,11 +15,16 @@ export default { | |||
// -----adhub----- | |||
'menu.dashboard': '总览', | |||
'menu.resource': '资源配置', | |||
'menu.resource.organization': '组织资源', | |||
'menu.resource.createVirtualCluster': '创建组织资源', | |||
'menu.resource.editVirtualCluster': '编辑组织资源', | |||
'menu.resource.userGroup': '用户组资源', | |||
'menu.resource.createUserGroup': '创建用户组资源', | |||
'menu.resource.editUserGroup': '编辑用户组资源', | |||
'menu.resourceManagement': '资源管理', | |||
'menu.resourceManagement.monitor': '资源监控', | |||
'menu.resourceManagement.alert': '系统告警', | |||
'menu.resourceManagement.createRule': '创建告警规则', | |||
'menu.resourceManagement.editRule': '编辑告警规则', | |||
'menu.resourceAllocation': '资源配置', | |||
'menu.resourceAllocation.organization': '组织资源', | |||
'menu.resourceAllocation.createOrganization': '创建组织资源', | |||
'menu.resourceAllocation.editOrganization': '编辑组织资源', | |||
'menu.resourceAllocation.userGroup': '用户组资源', | |||
'menu.resourceAllocation.createUserGroup': '创建用户组资源', | |||
'menu.resourceAllocation.editUserGroup': '编辑用户组资源', | |||
}; |
@@ -1,4 +1,5 @@ | |||
import { getPlatformConfig } from '../services/common'; | |||
import type { Reducer, Effect } from 'umi'; | |||
export const locales = ['zh-CN', 'en-US']; | |||
@@ -15,17 +16,17 @@ export interface CommonModelType { | |||
state: CommonStateType; | |||
effects: { | |||
changeTop: Effect; | |||
changeProjectId: Effect; | |||
changeProjectName: Effect; | |||
changeInterval: Effect; | |||
// changeProjectId: Effect; | |||
// changeProjectName: Effect; | |||
// changeInterval: Effect; | |||
fetchPlatformConfig: Effect; | |||
fetchPrivilegeJobStatus: Effect; | |||
// fetchPrivilegeJobStatus: Effect; | |||
}; | |||
reducers: { | |||
updateTop: Reducer; | |||
updateProjectId: Reducer; | |||
updateProjectName: Reducer; | |||
updateInterval: Reducer; | |||
// updateProjectId: Reducer; | |||
// updateProjectName: Reducer; | |||
// updateInterval: Reducer; | |||
savePlatform: Reducer; | |||
}; | |||
} | |||
@@ -14,6 +14,7 @@ export interface UserStateType { | |||
phone?: string, | |||
email?: string, | |||
currentVC: string[], | |||
userType?: number, | |||
}, | |||
} | |||
@@ -5,9 +5,9 @@ import { history, useParams } from 'umi'; | |||
import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons'; | |||
import { INPUT_MAX_LENGTH, FORM_COMMON_NAME_REG } from '@apulis/constants'; | |||
import { fetchVcLeft, fetchVcDetail, addVc, editVc } from './services'; | |||
import { GI_BIT_UNIT } from '@/utils/const'; | |||
import styles from './AddAndEdit.less'; | |||
import { GI_BIT_UNIT } from '../ResourceManage/list'; | |||
const FormItem = Form.Item; | |||
const FormList = Form.List; |
@@ -1,4 +1,4 @@ | |||
import React, { useEffect, useState, useRef } from 'react'; | |||
import React, { useRef } from 'react'; | |||
import { PageHeaderWrapper } from '@ant-design/pro-layout'; | |||
import { Button, Card, message, Modal, Divider, Popconfirm } from 'antd'; | |||
import { history, Link } from 'umi'; | |||
@@ -9,7 +9,7 @@ import { INPUT_MAX_LENGTH } from '@apulis/constants'; | |||
import { aomRequest as request } from '@/utils/request'; | |||
import { delVc, fetchVcStatus, fetchVCListApiPath } from './services'; | |||
import { GI_BIT_UNIT } from '../ResourceManage/list'; | |||
import { GI_BIT_UNIT } from '@/utils/const'; | |||
export type TableParamsType = { | |||
pageNum: number; |
@@ -0,0 +1,310 @@ | |||
import React, { Fragment, useState, useEffect, useRef } from 'react'; | |||
import { | |||
fetchResourceRuleDetail, | |||
handleResourceHistory, | |||
deleteResourceHistory, | |||
resourceHistoryAPI, | |||
} from './service'; | |||
import { | |||
Popover, | |||
Button, | |||
message, | |||
Divider, | |||
Popconfirm, | |||
Alert, | |||
Tag, | |||
Modal, | |||
} from 'antd'; | |||
import ATable from '@apulis/table'; | |||
import { useIntl } from 'umi'; | |||
import moment from 'moment'; | |||
import { ExclamationCircleOutlined } from '@ant-design/icons'; | |||
import { TIMEFORMAT, AOM_PREFIX } from '@/utils/const'; | |||
import { aomRequest } from '@/utils/request'; | |||
import DetailModal from './components/RuleDetail'; | |||
import { getConditionText } from './util'; | |||
import HandleModal from './components/HandleModal'; | |||
import { AuthzHOC } from '@apulis/authz'; | |||
import type { ATableColumn } from '@apulis/table'; | |||
import type { ActionType } from '@apulis/table/lib/typing'; | |||
import type { DataType } from './model'; | |||
import type { RulesType } from './AlertRules'; | |||
import styles from './index.less'; | |||
const STATUS_CONFIG = { | |||
0: { text: '待解决', color: '#999' }, | |||
1: { text: '已解决', color: '#2db7f5' }, | |||
2: { text: '未解决', color: '#f50' }, | |||
}; | |||
interface HistoryProps { | |||
computeType: DataType[]; | |||
conditionOp: DataType[]; | |||
sourceAndType: DataType[]; | |||
level: DataType[]; | |||
} | |||
interface ReceiversType { | |||
success: string[]; | |||
failed: string[]; | |||
} | |||
export interface HistoryType { | |||
uuid: string; | |||
name: string; | |||
createdAt: number; | |||
condition: any; | |||
receivers: ReceiversType; | |||
status: number; | |||
rule: string; | |||
} | |||
const AlarmHistory: React.FC<HistoryProps> = props => { | |||
const { formatMessage } = useIntl(); | |||
const { computeType, conditionOp, sourceAndType } = props; | |||
const [visible, setVisible] = useState({ detail: false, handle: false }); | |||
const [ruleDetail, setRuleDetail] = useState<RulesType | {}>({}); | |||
const [curRecord, setCurRecord] = useState<HistoryType | {}>({}); | |||
const [loading, setLoading] = useState({ handle: false }); | |||
const [selectKeys, setSelectKeys] = useState<React.Key[]>([]); | |||
const tableRef = useRef<ActionType<HistoryType>>(); | |||
const renderReceivers = (receivers: ReceiversType) => { | |||
const successDom = receivers?.success?.map(i => ( | |||
<div className={styles.receivers} key={i}>{i}<span className={styles.success}>发送成功</span></div> | |||
)); | |||
const failedDom = receivers?.failed?.map(i => ( | |||
<div className={styles.receivers} key={i}>{i}<span className={styles.failed}>发送失败</span></div> | |||
)); | |||
return <>{successDom}{failedDom}</>; | |||
}; | |||
const getRuleDetail = async (record: HistoryType) => { | |||
const hide = message.loading('加载中...'); | |||
const params = { id: record.rule }; | |||
const res = await fetchResourceRuleDetail(params); | |||
hide(); | |||
if (res && res.code === 0) { | |||
setRuleDetail(res.data); | |||
setVisible({ ...visible, detail: true }); | |||
} | |||
}; | |||
const onHandleClick = (record: HistoryType) => { | |||
setCurRecord(record); | |||
setVisible({ ...visible, handle: true }); | |||
}; | |||
const onHandleSubmit = async (values: { status: number }) => { | |||
setLoading({ ...loading, handle: true }); | |||
const params = { | |||
uuid: (curRecord as HistoryType).uuid, | |||
...values, | |||
}; | |||
try { | |||
const res = await handleResourceHistory(params); | |||
console.log(res); | |||
setLoading({ ...loading, handle: false }); | |||
if (res && res.code === 0) { | |||
message.success('操作成功'); | |||
tableRef.current?.reload(); | |||
setVisible({ ...visible, handle: false }); | |||
} | |||
} catch (err) { | |||
console.log(err); | |||
} | |||
}; | |||
const onBatchDelete = () => { | |||
Modal.confirm({ | |||
title: '确定删除所选项吗?', | |||
icon: <ExclamationCircleOutlined />, | |||
onOk: () => deleteHandle(false), | |||
okType: 'danger', | |||
}); | |||
}; | |||
const deleteHandle = async (record: HistoryType | boolean) => { | |||
const hide = message.loading('正在删除...'); | |||
const params = { | |||
uuids: record && (record as HistoryType).uuid ? [(record as HistoryType).uuid] : selectKeys, | |||
}; | |||
try { | |||
const res = await deleteResourceHistory(params); | |||
hide(); | |||
if (res && res.code === 0) { | |||
message.success('删除成功'); | |||
tableRef.current?.setPageNum((curR, pageNum) => { | |||
console.log(curR); | |||
if (curR.length === 1) { | |||
return pageNum - 1; | |||
} | |||
tableRef.current?.reload(); | |||
return record ? pageNum : 1; | |||
}) | |||
setSelectKeys([]); | |||
} | |||
} catch (err) { | |||
console.log(err); | |||
hide(); | |||
} | |||
}; | |||
const columns: ATableColumn<HistoryType>[] = [{ | |||
title: '告警ID', | |||
dataIndex: 'uuid', | |||
fixed: 'left', | |||
width: 300, | |||
}, { | |||
title: '告警规则名称', | |||
dataIndex: 'name', | |||
fixed: 'left', | |||
width: 200, | |||
render: (text, record) => ( | |||
<div style={{ wordWrap: 'break-word', wordBreak: 'break-word' }}><a onClick={() => getRuleDetail(record)}>{text}</a></div> | |||
), | |||
}, { | |||
title: '告警时间', | |||
dataIndex: 'createdAt', | |||
// format: 'time', | |||
render: text => text ? moment(text).format(TIMEFORMAT) : null, | |||
}, { | |||
title: '触发条件', | |||
dataIndex: 'condition', | |||
render: (text, record) => getConditionText(record, { | |||
computeType, | |||
conditionOp, | |||
sourceAndType, | |||
}), | |||
}, { | |||
title: '告警通知', | |||
dataIndex: 'receivers', | |||
render: text => text && text.success ? ( | |||
<Popover title='告警通知' content={() => renderReceivers(text)} color='#fff'> | |||
<span className={styles.success}>发送成功:{text?.success?.length} 条;</span> | |||
<span className={styles.failed}>发送失败:{text?.failed?.length} 条;</span> | |||
</Popover> | |||
) : null, | |||
}, { | |||
title: '告警状态', | |||
dataIndex: 'status', | |||
fixed: 'right', | |||
render: text => <Tag color={STATUS_CONFIG[text]?.color || '#999'}>{STATUS_CONFIG[text]?.text || '待解决'}</Tag>, | |||
}, { | |||
title: '操作', | |||
dataIndex: 'operation', | |||
align: 'right', | |||
fixed: 'right', | |||
render: (_, record) => { | |||
return ( | |||
<Fragment> | |||
<AuthzHOC anyOf={[ | |||
['/aom/api/v1/alert/service-history/{id}', 'put'], | |||
['/aom/api/v1/alert/resource-history/{id}', 'put'], | |||
]}> | |||
<a onClick={() => onHandleClick(record)}>处理</a> | |||
</AuthzHOC> | |||
<AuthzHOC anyOf={[ | |||
['/aom/api/v1/alert/service-history', 'delete'], | |||
['/aom/api/v1/alert/resource-history', 'delete'], | |||
]}> | |||
<Divider type='vertical' /> | |||
<Popconfirm | |||
title='确定删除此记录吗?' | |||
placement='topRight' | |||
onConfirm={() => deleteHandle(record)} | |||
> | |||
<a className={styles.dangerText}>删除</a> | |||
</Popconfirm> | |||
</AuthzHOC> | |||
</Fragment> | |||
) | |||
} | |||
}]; | |||
return ( | |||
<Fragment> | |||
{selectKeys.length > 0 ? | |||
<Alert | |||
message={`已选中 ${selectKeys.length} 项`} | |||
showIcon | |||
className={styles.alert} | |||
closeText={<a onClick={() => setSelectKeys([])}>取消选中</a>} | |||
/> : null | |||
} | |||
<ATable | |||
actionRef={tableRef} | |||
leftToolBar={ | |||
<AuthzHOC anyOf={[ | |||
[AOM_PREFIX + '/alert/resource-history', 'delete'], | |||
]}> | |||
<Button | |||
type='primary' | |||
danger | |||
onClick={onBatchDelete} | |||
disabled={selectKeys.length <= 0} | |||
>批量删除</Button> | |||
</AuthzHOC> | |||
} | |||
request={aomRequest} | |||
columns={columns} | |||
dataSourceAPIPath={resourceHistoryAPI} | |||
rowKey='uuid' | |||
rowSelection={{ | |||
selectedRowKeys: selectKeys, | |||
preserveSelectedRowKeys: true, | |||
onChange: keys => { | |||
console.log(keys); | |||
setSelectKeys(keys) | |||
}, | |||
}} | |||
scroll={{ x: 'max-content' }} | |||
searchToolBar={[ | |||
{ | |||
type: 'dateRange', | |||
name: 'time', | |||
nameArr: ['startTime', 'endTime'], | |||
// timeFormat: 'ms', | |||
formItemProps: { | |||
initialValue: [moment().subtract(24, 'hour'), moment()], | |||
}, | |||
dateRangeProps: { | |||
showTime: true, | |||
disabledDate: current => current > moment().endOf('day'), | |||
style: { width: 350 }, | |||
allowClear: false, | |||
} | |||
}, | |||
{ | |||
name: 'name', | |||
type: 'input', | |||
placeholder: '请输入告警规则名称', | |||
inputProps: { | |||
style: { width: 200 } | |||
} | |||
}, | |||
]} | |||
/> | |||
<DetailModal | |||
title='告警规则详情' | |||
visible={visible.detail} | |||
cancel={() => setVisible({ ...visible, detail: false })} | |||
detail={ruleDetail as RulesType} | |||
{...props} | |||
/> | |||
<HandleModal | |||
visible={visible.handle} | |||
cancel={() => setVisible({ ...visible, handle: false })} | |||
curRecord={curRecord as HistoryType} | |||
loading={loading.handle} | |||
afterClose={() => setCurRecord({})} | |||
onSubmit={onHandleSubmit} | |||
/> | |||
</Fragment > | |||
) | |||
} | |||
export default AlarmHistory; |
@@ -0,0 +1,200 @@ | |||
import React, { Fragment, useState, useRef } from 'react'; | |||
import { | |||
resourceRulesAPI, | |||
fetchResourceRuleDetail, | |||
changeResourceRuleStatus, | |||
deleteResourceRule, | |||
} from './service'; | |||
import { Button, Divider, Tag, message, Popconfirm } from 'antd'; | |||
import ATable from '@apulis/table'; | |||
import { useIntl, Link } from 'umi'; | |||
import { AOM_PREFIX } from '@/utils/const'; | |||
import DetailModal from './components/RuleDetail'; | |||
import { formatSeconds } from '@/utils/utils'; | |||
import { getText, getConditionText } from './util'; | |||
import { aomRequest } from '@/utils/request'; | |||
import { AuthzHOC } from '@apulis/authz'; | |||
import type { ATableColumn } from '@apulis/table'; | |||
import type { ActionType } from '@apulis/table/lib/typing'; | |||
import type { DataType } from './model'; | |||
import styles from './index.less'; | |||
interface RulesProps { | |||
sourceAndType: DataType[]; | |||
computeType: DataType[]; | |||
conditionOp: DataType[]; | |||
level: DataType[]; | |||
} | |||
export interface RulesType { | |||
id: string | number; | |||
name: string; | |||
uuid: string; | |||
status: number; | |||
source: string; | |||
frequency: number; | |||
triggerCount: number; | |||
interval: number; | |||
type: string; | |||
level: string; | |||
title: string; | |||
updatedAt: number; | |||
content: string; | |||
} | |||
const AlarmRules: React.FC<RulesProps> = props => { | |||
const { formatMessage } = useIntl(); | |||
const { | |||
sourceAndType, | |||
computeType, | |||
conditionOp, | |||
} = props; | |||
const [visible, setVisible] = useState({ detail: false }); | |||
const [ruleDetail, setRuleDetail] = useState<RulesType | {}>({}); | |||
const tableRef = useRef<ActionType<RulesType>>(); | |||
const getRuleDetail = async (record: RulesType) => { | |||
const hide = message.loading('加载中...'); | |||
const params = { id: record.uuid }; | |||
const res = await fetchResourceRuleDetail(params); | |||
hide(); | |||
if (res && res.code === 0) { | |||
setRuleDetail(res.data); | |||
setVisible({ ...visible, detail: true }); | |||
} | |||
}; | |||
const onStatusChange = async (record: RulesType) => { | |||
const hide = message.loading('正在更改...'); | |||
const params = { id: record.uuid, status: record.status === 1 ? 0 : 1 }; | |||
const res = await changeResourceRuleStatus(params); | |||
hide(); | |||
if (res && res.code === 0) { | |||
message.success('状态更改成功'); | |||
tableRef.current?.reload(); | |||
} | |||
}; | |||
const onDelete = async (record: RulesType) => { | |||
const hide = message.loading('正在删除...'); | |||
const params = { id: record.uuid }; | |||
const res = await deleteResourceRule(params); | |||
hide(); | |||
if (res && res.code === 0) { | |||
message.success(res.data.msg || '删除成功'); | |||
tableRef.current?.setPageNum((curR, pageNum) => { | |||
console.log(curR); | |||
if (curR.length === 1) { | |||
return pageNum - 1; | |||
} | |||
tableRef.current?.reload(); | |||
return record ? pageNum : 1; | |||
}); | |||
} | |||
}; | |||
const columns: ATableColumn<RulesType>[] = [{ | |||
title: '规则名称', | |||
dataIndex: 'name', | |||
fixed: 'left', | |||
width: 250, | |||
render: (text, record) => ( | |||
<div style={{ wordWrap: 'break-word', wordBreak: 'break-word' }}><a onClick={() => getRuleDetail(record)}>{text}</a></div> | |||
), | |||
}, { | |||
title: '告警来源', | |||
dataIndex: 'source', | |||
render: text => getText(sourceAndType, text), | |||
}, { | |||
title: '告警类型', | |||
dataIndex: 'type', | |||
render: (text, record) => { | |||
const data = sourceAndType?.find(i => i.value === record.source)?.children || []; | |||
return getText(data, text); | |||
}, | |||
}, { | |||
title: '告警条件', | |||
dataIndex: 'condition', | |||
render: (text, record) => getConditionText(record, { | |||
computeType, | |||
conditionOp, | |||
}), | |||
}, { | |||
title: '告警频率', | |||
dataIndex: 'frequency', | |||
render: text => `${formatSeconds(text)?.label}`, | |||
}, { | |||
title: '触发通知阈值', | |||
dataIndex: 'triggerCount', | |||
render: text => `${text}次`, | |||
}, { | |||
title: '通知间隔', | |||
dataIndex: 'interval', | |||
render: text => `${formatSeconds(text)?.label}`, | |||
}, { | |||
title: '告警通道', | |||
dataIndex: 'receivers', | |||
render: () => '邮箱', // 当前固定为邮箱 | |||
}, { | |||
title: '状态', | |||
dataIndex: 'status', | |||
render: text => <Tag color={text === 1 ? '#87d068' : '#f50'}>{text === 1 ? '生效中' : '未生效'}</Tag>, | |||
}, { | |||
title: '操作', | |||
dataIndex: 'operation', | |||
align: 'right', | |||
fixed: 'right', | |||
render: (text, record) => ( | |||
<Fragment> | |||
{record.status !== 1 && ( | |||
<Fragment> | |||
<Link to={`/resourceManagement/alert/edit/${record.uuid}`}>编辑</Link> | |||
<Divider type='vertical' /> | |||
</Fragment> | |||
)} | |||
<a | |||
className={record.status === 1 ? styles.dangerText : ''} | |||
onClick={() => onStatusChange(record)} | |||
>{record.status === 1 ? '停止' : '生效'}</a> | |||
{record.status !== 1 && ( | |||
<Fragment> | |||
<Divider type='vertical' /> | |||
<Popconfirm title={`确定删除“${record.name}”吗?`} onConfirm={() => onDelete(record)} placement='topRight'> | |||
<a className={styles.dangerText}>删除</a> | |||
</Popconfirm> | |||
</Fragment> | |||
)} | |||
</Fragment> | |||
) | |||
}]; | |||
return ( | |||
<Fragment> | |||
<ATable | |||
actionRef={tableRef} | |||
leftToolBar={ | |||
<AuthzHOC permission={`${AOM_PREFIX}/alert/resource-rules`} action='post'> | |||
<Link to='/resourceManagement/alert/create'> | |||
<Button type='primary'>创建告警规则</Button> | |||
</Link> | |||
</AuthzHOC> | |||
} | |||
request={aomRequest} | |||
columns={columns} | |||
dataSourceAPIPath={resourceRulesAPI} | |||
scroll={{ x: 'max-content' }} | |||
/> | |||
<DetailModal | |||
title='告警规则详情' | |||
visible={visible.detail} | |||
cancel={() => setVisible({ ...visible, detail: false })} | |||
detail={ruleDetail as RulesType} | |||
{...props} | |||
/> | |||
</Fragment> | |||
) | |||
} | |||
export default AlarmRules; |
@@ -0,0 +1,168 @@ | |||
import React, { useEffect, useState } from 'react'; | |||
import { PageHeaderWrapper } from '@ant-design/pro-layout'; | |||
import { PageHeader, Card, message } from 'antd'; | |||
import { useIntl, history, connect } from 'umi'; | |||
import { FORM_CONFIG } from './const'; | |||
import RuleForm from './components/RuleForm'; | |||
import { | |||
createResourceRule, | |||
fetchResourceRuleDetail, | |||
editResourceRule, | |||
getCurUserEmail, | |||
testEmail, | |||
} from './service'; | |||
import { formatTime2Seconds, formatSeconds } from '@/utils/utils'; | |||
import type { ConnectProps, Dispatch } from 'umi'; | |||
import type { AlertStateType } from './model'; | |||
import type { SubmitValuesType, ReceiversType } from './components/RuleForm'; | |||
interface CreateAndEditProps extends ConnectProps { | |||
dispatch: Dispatch; | |||
alert: AlertStateType; | |||
} | |||
const CreateRule: React.FC<CreateAndEditProps> = props => { | |||
const { formatMessage } = useIntl(); | |||
const { pathname } = props.location; | |||
const { id: ruleId } = props.match?.params as { id: string }; | |||
const addInitialValue = { | |||
frequencyAfter: 'minute', | |||
threshold: 1, | |||
intervalAfter: 'now', | |||
level: 'urgency', | |||
receivers: [{ hannel: 'mail' }], | |||
dataFilter: { | |||
rangeAfter: FORM_CONFIG?.dataSelectAfter[0]?.value, | |||
}, | |||
condition: { | |||
operator: 'gt', | |||
value: 70, | |||
}, | |||
}; | |||
const [loading, setLoading] = useState(false); | |||
const [initialValue, setInitialValue] = useState<SubmitValuesType>(addInitialValue); | |||
const getRuleDetail = async () => { | |||
const hide = message.loading('加载中...'); | |||
const params = { id: ruleId }; | |||
const res = await fetchResourceRuleDetail(params); | |||
hide(); | |||
if (res && res.code === 0) { | |||
const { data } = res; | |||
setInitialValue({ | |||
...data, | |||
dataFilter: { | |||
...data.dataFilter, | |||
range: formatSeconds(data.dataFilter.range)?.value, | |||
rangeAfter: formatSeconds(data.dataFilter.range)?.unit, | |||
}, | |||
interval: formatSeconds(data.interval)?.value, | |||
intervalAfter: formatSeconds(data.interval)?.unit, | |||
frequency: formatSeconds(data.frequency)?.value, | |||
frequencyAfter: formatSeconds(data.frequency)?.unit, | |||
receivers: data.receivers?.split(';').map((i: string) => ({ // 当前使用字符串,类型固定为邮箱 | |||
hannel: 'mail', | |||
value: i, | |||
})), | |||
}); | |||
} | |||
}; | |||
useEffect(() => { | |||
const { dispatch } = props; | |||
dispatch({ type: 'alarm/getMetaData' }); | |||
if (ruleId) { | |||
getRuleDetail(); | |||
} | |||
}, []); | |||
const onSubmit = async (values: SubmitValuesType) => { | |||
setLoading(true); | |||
try { | |||
const params = { | |||
...values, | |||
id: ruleId || undefined, | |||
triggerCount: Number(values.triggerCount), | |||
dataFilter: { | |||
...values.dataFilter, | |||
range: formatTime2Seconds(values.dataFilter.range, values.dataFilter.rangeAfter), | |||
rangeAfter: undefined, | |||
}, | |||
interval: formatTime2Seconds(values.interval || 0, values.intervalAfter), | |||
intervalAfter: undefined, | |||
frequency: formatTime2Seconds(values.frequency, values.frequencyAfter), | |||
frequencyAfter: undefined, | |||
// 当前使用字符串,类型固定为邮箱 | |||
receivers: (values.receivers as ReceiversType[]).map((i) => i.value).join(';'), | |||
} | |||
const res = ruleId ? await editResourceRule(params) : await createResourceRule(params); | |||
setLoading(false); | |||
if (res && res.code === 0) { | |||
message.success('操作成功'); | |||
history.push(`/resourceManagement/alert/list?tab=2`); | |||
} | |||
} catch (err) { | |||
setLoading(false); | |||
console.log(err); | |||
} | |||
}; | |||
const sendEmail = async (value: string) => { | |||
const checkHide = message.loading('加载中...'); | |||
try { | |||
const checkRes = await getCurUserEmail(); | |||
checkHide(); | |||
if (checkRes && checkRes.code === 0) { | |||
if (checkRes.data && checkRes.data.email) { | |||
const hide = message.loading('正在发送...'); | |||
try { | |||
const res = await testEmail({ email: value }); | |||
hide(); | |||
if (res && res.code === 0) { | |||
message.success('发送成功'); | |||
} else { | |||
message.error('发送失败,请稍后重试'); | |||
} | |||
} catch (err) { | |||
hide(); | |||
console.log(err); | |||
} | |||
} else { | |||
message.error('请先在系统设置中设置发件箱!'); | |||
} | |||
} else if (checkRes.code === 500020005) { | |||
message.error('请先在系统设置中设置发件箱!'); | |||
} | |||
} catch (err) { | |||
checkHide(); | |||
console.log(err); | |||
} | |||
}; | |||
return ( | |||
<PageHeaderWrapper title={false}> | |||
<PageHeader | |||
ghost={false} | |||
onBack={() => history.push('/resourceManagement/alert/list?tab=2')} | |||
title={formatMessage({ id: `menu.resourceManagement.${ruleId ? 'editRule' : 'createRule'}` })} | |||
/> | |||
<Card style={{ marginTop: 24 }}> | |||
<RuleForm | |||
initialValue={initialValue} | |||
onSubmit={onSubmit} | |||
sendEmail={sendEmail} | |||
loading={loading} | |||
isEdit={!!ruleId} | |||
{...props.alert} | |||
/> | |||
</Card> | |||
</PageHeaderWrapper> | |||
) | |||
} | |||
export default connect(({ alert }: { alert: AlertStateType }) => ({ | |||
alert, | |||
}))(CreateRule); |
@@ -0,0 +1,492 @@ | |||
function getHistoryList(req, res) { | |||
return res.json({ | |||
"code": 0, | |||
"data": { | |||
"items": [ | |||
{ | |||
"id": 338, | |||
"createdAt": 1627609426785, | |||
"updatedAt": 1627609427601, | |||
"uuid": "bee5ddeb-1ebb-452a-b6d2-398d0009f024", | |||
"orgId": 1946593, | |||
"name": "FF91111111123", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 120 | |||
}, | |||
"condition": { | |||
"value": 70, | |||
"operator": "lt" | |||
}, | |||
"rule": "f19e400e-0833-4369-b04b-682033f2f020", | |||
"receivers": { | |||
"success": [ | |||
"464786370@qq.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"instance": "123.60.231.99:9100", | |||
"alertname": "FF91111111123" | |||
}, | |||
"type": "cpuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 337, | |||
"createdAt": 1627609426780, | |||
"updatedAt": 1627609427638, | |||
"uuid": "2e6b7b8b-efdd-48e2-a860-e4c74dfeb989", | |||
"orgId": 1946593, | |||
"name": "FF91111111123", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 120 | |||
}, | |||
"condition": { | |||
"value": 70, | |||
"operator": "lt" | |||
}, | |||
"rule": "f19e400e-0833-4369-b04b-682033f2f020", | |||
"receivers": { | |||
"success": [ | |||
"464786370@qq.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"instance": "123.60.231.101:9100", | |||
"alertname": "FF91111111123" | |||
}, | |||
"type": "cpuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 336, | |||
"createdAt": 1627609426776, | |||
"updatedAt": 1627609427554, | |||
"uuid": "dff492f0-a429-4b44-834c-232f3fcd0e15", | |||
"orgId": 1946593, | |||
"name": "FF91111111123", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 120 | |||
}, | |||
"condition": { | |||
"value": 70, | |||
"operator": "lt" | |||
}, | |||
"rule": "f19e400e-0833-4369-b04b-682033f2f020", | |||
"receivers": { | |||
"success": [ | |||
"464786370@qq.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"instance": "123.60.231.100:9100", | |||
"alertname": "FF91111111123" | |||
}, | |||
"type": "cpuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 335, | |||
"createdAt": 1627609426775, | |||
"updatedAt": 1627609427691, | |||
"uuid": "e8ee6f72-82ac-4787-9259-8d2050558208", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"rule": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"receivers": { | |||
"success": [ | |||
"wei.lin@apulis.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"id": "4", | |||
"app": "prometheus", | |||
"job": "kubernetes-service-endpoints", | |||
"chart": "prometheus-14.2.1", | |||
"release": "prometheus", | |||
"heritage": "Helm", | |||
"instance": "123.60.231.99:9200", | |||
"alertname": "demo", | |||
"component": "npu-exporter", | |||
"kubernetes_name": "prometheus-npu-exporter", | |||
"kubernetes_node": "123.60.231.99", | |||
"kubernetes_namespace": "kube-system", | |||
"app_kubernetes_io_managed_by": "Helm" | |||
}, | |||
"type": "npuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 334, | |||
"createdAt": 1627609426772, | |||
"updatedAt": 1627609427637, | |||
"uuid": "87614ba4-3116-45d3-9382-f374ebb32128", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"rule": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"receivers": { | |||
"success": [ | |||
"wei.lin@apulis.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"id": "1", | |||
"app": "prometheus", | |||
"job": "kubernetes-service-endpoints", | |||
"chart": "prometheus-14.2.1", | |||
"release": "prometheus", | |||
"heritage": "Helm", | |||
"instance": "123.60.231.99:9200", | |||
"alertname": "demo", | |||
"component": "npu-exporter", | |||
"kubernetes_name": "prometheus-npu-exporter", | |||
"kubernetes_node": "123.60.231.99", | |||
"kubernetes_namespace": "kube-system", | |||
"app_kubernetes_io_managed_by": "Helm" | |||
}, | |||
"type": "npuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 333, | |||
"createdAt": 1627609426770, | |||
"updatedAt": 1627609427654, | |||
"uuid": "4e8bb55b-352d-44a7-84ca-49674a3ced22", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"rule": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"receivers": { | |||
"success": [ | |||
"wei.lin@apulis.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"id": "9", | |||
"app": "prometheus", | |||
"job": "kubernetes-service-endpoints", | |||
"chart": "prometheus-14.2.1", | |||
"release": "prometheus", | |||
"heritage": "Helm", | |||
"instance": "123.60.231.101:9200", | |||
"alertname": "demo", | |||
"component": "npu-exporter", | |||
"kubernetes_name": "prometheus-npu-exporter", | |||
"kubernetes_node": "123.60.231.101", | |||
"kubernetes_namespace": "kube-system", | |||
"app_kubernetes_io_managed_by": "Helm" | |||
}, | |||
"type": "npuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 332, | |||
"createdAt": 1627609426768, | |||
"updatedAt": 1627609427674, | |||
"uuid": "746b2a8c-a0e3-4652-ae69-5ffc269a3c0f", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"rule": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"receivers": { | |||
"success": [ | |||
"wei.lin@apulis.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"id": "8", | |||
"app": "prometheus", | |||
"job": "kubernetes-service-endpoints", | |||
"chart": "prometheus-14.2.1", | |||
"release": "prometheus", | |||
"heritage": "Helm", | |||
"instance": "123.60.231.101:9200", | |||
"alertname": "demo", | |||
"component": "npu-exporter", | |||
"kubernetes_name": "prometheus-npu-exporter", | |||
"kubernetes_node": "123.60.231.101", | |||
"kubernetes_namespace": "kube-system", | |||
"app_kubernetes_io_managed_by": "Helm" | |||
}, | |||
"type": "npuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 331, | |||
"createdAt": 1627609426766, | |||
"updatedAt": 1627609427645, | |||
"uuid": "71642004-648c-4caf-a453-cd0fa3ff865f", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"rule": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"receivers": { | |||
"success": [ | |||
"wei.lin@apulis.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"id": "7", | |||
"app": "prometheus", | |||
"job": "kubernetes-service-endpoints", | |||
"chart": "prometheus-14.2.1", | |||
"release": "prometheus", | |||
"heritage": "Helm", | |||
"instance": "123.60.231.101:9200", | |||
"alertname": "demo", | |||
"component": "npu-exporter", | |||
"kubernetes_name": "prometheus-npu-exporter", | |||
"kubernetes_node": "123.60.231.101", | |||
"kubernetes_namespace": "kube-system", | |||
"app_kubernetes_io_managed_by": "Helm" | |||
}, | |||
"type": "npuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 330, | |||
"createdAt": 1627609426763, | |||
"updatedAt": 1627609427681, | |||
"uuid": "bde40d95-b0b0-4a31-99a6-2d596c4e2ef8", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"rule": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"receivers": { | |||
"success": [ | |||
"wei.lin@apulis.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"id": "6", | |||
"app": "prometheus", | |||
"job": "kubernetes-service-endpoints", | |||
"chart": "prometheus-14.2.1", | |||
"release": "prometheus", | |||
"heritage": "Helm", | |||
"instance": "123.60.231.101:9200", | |||
"alertname": "demo", | |||
"component": "npu-exporter", | |||
"kubernetes_name": "prometheus-npu-exporter", | |||
"kubernetes_node": "123.60.231.101", | |||
"kubernetes_namespace": "kube-system", | |||
"app_kubernetes_io_managed_by": "Helm" | |||
}, | |||
"type": "npuUsage", | |||
"source": "node", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 329, | |||
"createdAt": 1627609426760, | |||
"updatedAt": 1627609427655, | |||
"uuid": "0d32adce-f85f-476c-9bec-a908b3e4c7d9", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"level": "urgency", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"rule": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"receivers": { | |||
"success": [ | |||
"wei.lin@apulis.com" | |||
], | |||
"failed": [ | |||
] | |||
}, | |||
"labels": { | |||
"id": "5", | |||
"app": "prometheus", | |||
"job": "kubernetes-service-endpoints", | |||
"chart": "prometheus-14.2.1", | |||
"release": "prometheus", | |||
"heritage": "Helm", | |||
"instance": "123.60.231.101:9200", | |||
"alertname": "demo", | |||
"component": "npu-exporter", | |||
"kubernetes_name": "prometheus-npu-exporter", | |||
"kubernetes_node": "123.60.231.101", | |||
"kubernetes_namespace": "kube-system", | |||
"app_kubernetes_io_managed_by": "Helm" | |||
}, | |||
"type": "npuUsage", | |||
"source": "node", | |||
"status": 0 | |||
} | |||
], | |||
"total": 50 | |||
}, | |||
"msg": "ok.ActionSuccess" | |||
}); | |||
} | |||
function getRuleList(req, res) { | |||
return res.json({ | |||
"code": 0, | |||
"data": { | |||
"items": [ | |||
{ | |||
"id": 2, | |||
"createdAt": 1627452727649, | |||
"updatedAt": 1627609480721, | |||
"uuid": "f19e400e-0833-4369-b04b-682033f2f020", | |||
"orgId": 1946593, | |||
"name": "FF91111111123", | |||
"source": "node", | |||
"type": "cpuUsage", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 120 | |||
}, | |||
"condition": { | |||
"value": 70, | |||
"operator": "lt" | |||
}, | |||
"level": "urgency", | |||
"frequency": 120, | |||
"triggerCount": 1, | |||
"interval": 0, | |||
"title": "FF91111111123告警", | |||
"content": "FF91111111123", | |||
"receivers": "464786370@qq.com", | |||
"status": 0 | |||
}, | |||
{ | |||
"id": 1, | |||
"createdAt": 1626080041722, | |||
"updatedAt": 1627609481677, | |||
"uuid": "c5bbd9eb-696b-4ff6-9b6c-beb8a1c489e4", | |||
"orgId": 1946593, | |||
"name": "demo", | |||
"source": "node", | |||
"type": "npuUsage", | |||
"dataFilter": { | |||
"type": "max", | |||
"range": 86400 | |||
}, | |||
"condition": { | |||
"value": 98, | |||
"operator": "lt" | |||
}, | |||
"level": "urgency", | |||
"frequency": 60, | |||
"triggerCount": 1, | |||
"interval": 0, | |||
"title": "test", | |||
"content": "sadfads", | |||
"receivers": "wei.lin@apulis.com", | |||
"status": 0 | |||
} | |||
], | |||
"total": 2 | |||
}, | |||
"msg": "ok.ActionSuccess" | |||
}); | |||
} | |||
const option = { | |||
code: 0, | |||
msg: 'success', | |||
}; | |||
export default { | |||
'GET /aom/api/v1/alert/resource-history': getHistoryList, | |||
'GET /aom/api/v1/alert/resource-rules': getRuleList, | |||
} |
@@ -0,0 +1,73 @@ | |||
import React, { useEffect } from 'react'; | |||
import { Modal, Form, Radio } from 'antd'; | |||
const FormItem = Form.Item; | |||
const commonLayout = { | |||
labelCol: { span: 6 }, | |||
wrapperCol: { span: 16 }, | |||
} | |||
interface SubmitValueType { | |||
status: number; | |||
} | |||
interface Props { | |||
visible: boolean; | |||
curRecord: { | |||
status: number; | |||
}; | |||
loading: boolean; | |||
afterClose: () => void; | |||
cancel: () => void; | |||
onSubmit: (values: SubmitValueType) => void; | |||
} | |||
const HandleModal: React.FC<Props> = props => { | |||
const { visible, loading, curRecord, afterClose, cancel, onSubmit } = props; | |||
const [form] = Form.useForm(); | |||
useEffect(() => { | |||
if (visible && form) { | |||
form.setFieldsValue({ status: curRecord.status || 1 }); | |||
} | |||
}, [curRecord, visible]); | |||
const onOk = () => { | |||
form.validateFields().then(values => { | |||
onSubmit(values); | |||
}); | |||
}; | |||
return ( | |||
<Modal | |||
title='告警处理' | |||
visible={visible} | |||
confirmLoading={loading} | |||
afterClose={() => { | |||
form.resetFields(); | |||
afterClose(); | |||
}} | |||
maskClosable={false} | |||
destroyOnClose | |||
onCancel={cancel} | |||
onOk={onOk} | |||
> | |||
<Form form={form} initialValues={{ status: 1 }} {...commonLayout}> | |||
<FormItem | |||
label='告警处理' | |||
name='status' | |||
rules={[ | |||
{ required: true, message: '请选择' } | |||
]} | |||
> | |||
<Radio.Group> | |||
<Radio value={1}>已解决</Radio> | |||
<Radio value={2}>未解决</Radio> | |||
</Radio.Group> | |||
</FormItem> | |||
</Form> | |||
</Modal> | |||
) | |||
} | |||
export default HandleModal; |
@@ -0,0 +1,66 @@ | |||
import React, { ReactNode } from 'react'; | |||
import { Modal, Descriptions, Button } from 'antd'; | |||
import { getText, getConditionText } from '../util'; | |||
import { formatSeconds } from '@/utils/utils'; | |||
import moment from 'moment'; | |||
import { TIMEFORMAT } from '@/utils/const'; | |||
import type { DataType } from '../model'; | |||
import type { RulesType } from '../AlertRules'; | |||
interface DetailProps { | |||
title: string | ReactNode; | |||
cancel: () => void; | |||
visible: boolean; | |||
detail: RulesType; | |||
sourceAndType: DataType[]; | |||
computeType: DataType[]; | |||
conditionOp: DataType[]; | |||
level: DataType[]; | |||
} | |||
const Detail: React.FC<DetailProps> = props => { | |||
const { | |||
title, | |||
cancel, | |||
visible, | |||
detail, | |||
sourceAndType, | |||
computeType, | |||
conditionOp, | |||
level, | |||
} = props; | |||
return ( | |||
<Modal | |||
title={title} | |||
visible={visible} | |||
destroyOnClose | |||
onCancel={cancel} | |||
maskClosable={false} | |||
width='80%' | |||
style={{ maxWidth: 1200 }} | |||
footer={<Button type='primary' onClick={cancel}>关闭</Button>} | |||
> | |||
<Descriptions bordered> | |||
<Descriptions.Item label='规则名称'>{detail.name}</Descriptions.Item> | |||
<Descriptions.Item label='告警来源'>{getText(sourceAndType, detail.source)}</Descriptions.Item> | |||
<Descriptions.Item label='告警类型'> | |||
{getText(sourceAndType?.find(i => i.value === detail.source)?.children || [], detail.type)} | |||
</Descriptions.Item> | |||
<Descriptions.Item label='告警条件' span={2}>{getConditionText(detail, { computeType, conditionOp })}</Descriptions.Item> | |||
<Descriptions.Item label='告警频率'>{formatSeconds(detail.frequency)?.label}</Descriptions.Item> | |||
<Descriptions.Item label='触发通知阈值'>{detail.triggerCount}次</Descriptions.Item> | |||
<Descriptions.Item label='通知间隔'>{formatSeconds(detail.interval)?.label}</Descriptions.Item> | |||
{/* 当前固定为邮箱 */} | |||
<Descriptions.Item label='告警通道'>邮箱</Descriptions.Item> | |||
<Descriptions.Item label='告警标题'>【{getText(level, detail.level)}】{detail.title}</Descriptions.Item> | |||
<Descriptions.Item label='状态'>{detail.status === 1 ? '生效中' : '未生效'}</Descriptions.Item> | |||
<Descriptions.Item label='更新时间'>{detail.updatedAt ? moment(detail.updatedAt).format(TIMEFORMAT) : null}</Descriptions.Item> | |||
<Descriptions.Item label='告警内容' span={3}>{detail.content}</Descriptions.Item> | |||
</Descriptions> | |||
</Modal> | |||
) | |||
} | |||
export default Detail; |
@@ -0,0 +1,394 @@ | |||
import React, { Fragment, ReactNode, useEffect, useState } from 'react'; | |||
import { Form, Button, Input, Select, InputNumber, Cascader, Tooltip, message } from 'antd'; | |||
import { FORM_CONFIG, COMMON_LAYOUT, TIME_DATA, INTERVAL_TYPES } from '../const'; | |||
import { QuestionCircleOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
import { emailReg } from '@/utils/reg'; | |||
import { getText } from '../util'; | |||
import type { DataType } from '../model'; | |||
import styles from '../index.less'; | |||
const FormItem = Form.Item; | |||
const { Option } = Select; | |||
const { TextArea } = Input; | |||
const getLabel = (value: string, data: DataType[]) => { | |||
return data.filter(i => i.value === value)[0]?.label; | |||
}; | |||
const getInfoIcon = (text: string | ReactNode) => { | |||
return <Tooltip title={text}><QuestionCircleOutlined className={styles.iconStyle} /></Tooltip> | |||
}; | |||
const getPatternRule = (type: string | number) => { | |||
let rule = {}; | |||
switch (type) { | |||
case 'mail': | |||
rule = emailReg; | |||
break; | |||
default: | |||
break; | |||
} | |||
return rule; | |||
}; | |||
export interface ReceiversType { | |||
hannel: string; | |||
value?: string; | |||
} | |||
export interface SubmitValuesType { | |||
id?: string | number; | |||
source?: string; | |||
triggerCount?: string | number; | |||
dataFilter: { | |||
range?: string | number; | |||
rangeAfter?: string; | |||
}, | |||
interval?: string | number; | |||
intervalAfter?: string; | |||
frequency?: string | number; | |||
frequencyAfter?: string; | |||
receivers: ReceiversType[] | string; | |||
threshold?: number; | |||
condition: { | |||
operator: string; | |||
value: number; | |||
} | |||
} | |||
interface RuleFormProps { | |||
initialValue: SubmitValuesType; | |||
sourceAndType: DataType[]; | |||
onSubmit: (values: SubmitValuesType) => void; | |||
sendEmail: (val: string) => void; | |||
loading: boolean; | |||
computeType: DataType[]; | |||
conditionOp: DataType[]; | |||
level: DataType[]; | |||
receiverChannel: DataType[]; | |||
isEdit: boolean; | |||
} | |||
const RuleForm: React.FC<RuleFormProps> = props => { | |||
const [form] = Form.useForm(); | |||
const { | |||
initialValue, | |||
onSubmit, | |||
sendEmail, | |||
loading, | |||
sourceAndType, | |||
computeType, | |||
conditionOp, | |||
level, | |||
receiverChannel, | |||
isEdit, | |||
} = props; | |||
const [curTypes, setCurTypes] = useState<DataType[]>([]); // 当前告警类型,根据告警来源计算 | |||
useEffect(() => { | |||
if (initialValue.source) { | |||
const curType = sourceAndType?.filter(i => i.value === initialValue.source)?.[0] || {}; | |||
setCurTypes(curType.children || []); | |||
} | |||
form.setFieldsValue(initialValue); | |||
}, [initialValue]); | |||
const onSourceChange = (value: string) => { | |||
const curType = sourceAndType?.filter(i => i.value === value)?.[0] || {}; | |||
setCurTypes(curType.children || []); | |||
form.setFieldsValue({ type: undefined }); | |||
}; | |||
const onTypeFocus = () => { | |||
if (!form.getFieldValue('source')) { | |||
message.warning('请先选择告警来源!'); | |||
} | |||
}; | |||
const onTestEmailClick = (field: { name: number | string }) => { | |||
const receivers = form.getFieldValue('receivers'); | |||
const curEmailInfo = receivers[field.name]; | |||
if (!curEmailInfo.value) { | |||
message.warning('请先输入邮箱地址!'); | |||
return; | |||
} | |||
if (!emailReg.pattern.test(curEmailInfo.value)) { | |||
message.warning('邮箱地址格式有误,请核对后输入!'); | |||
return; | |||
} | |||
sendEmail(curEmailInfo.value); | |||
}; | |||
const onIntervalAfterChange = (value: string) => { | |||
if (value === 'now') { | |||
form.setFieldsValue({ | |||
interval: undefined, | |||
}); | |||
} | |||
}; | |||
return ( | |||
<Form | |||
form={form} | |||
{...COMMON_LAYOUT} | |||
onFinish={onSubmit} | |||
scrollToFirstError | |||
> | |||
<FormItem | |||
label='规则名称' | |||
name='name' | |||
rules={[ | |||
{ required: true, message: '请输入告警规则名称' }, | |||
{ pattern: /^[\w\u4e00-\u9fa5]{1,100}$/, message: '请输入中英文或数字,不超过100个字符' }, | |||
]} | |||
> | |||
<Input placeholder='请输入告警规则名称' maxLength={101} /> | |||
</FormItem> | |||
<FormItem label='告警来源' required> | |||
<FormItem name='source' rules={[{ required: true, message: '请选择告警来源' }]} noStyle> | |||
<Select placeholder='请选择告警来源' onChange={onSourceChange}> | |||
{sourceAndType?.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
{ | |||
getInfoIcon(sourceAndType?.map(i => <div style={{ margin: '6px 0' }} key={i.value}>{`${i.label}:是指单个${i.label}产生的告警`}</div>)) | |||
} | |||
</FormItem> | |||
<FormItem label='告警类型' required> | |||
<FormItem name='type' rules={[{ required: true, message: '请选择告警类型' }]} noStyle> | |||
<Select placeholder='请选择告警类型' onFocus={onTypeFocus}> | |||
{curTypes.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
</FormItem> | |||
<FormItem label='告警数据' required style={{ marginBottom: 0 }}> | |||
<FormItem label={FORM_CONFIG.dataLabel} style={{ marginBottom: 0 }}> | |||
<FormItem | |||
name={['dataFilter', 'range']} | |||
style={{ display: 'inline-block', width: 'calc(100% - 80px)' }} | |||
rules={[ | |||
{ required: true, message: `请输入${FORM_CONFIG.dataLabel}` }, | |||
{ pattern: /^[0-9]+$/, message: '请输入整数' }, | |||
]} | |||
> | |||
<Input placeholder='请输入整数,例如1' /> | |||
</FormItem> | |||
<FormItem | |||
name={['dataFilter', 'rangeAfter']} | |||
style={{ display: 'inline-block', width: '80px' }} | |||
> | |||
<Select> | |||
{FORM_CONFIG?.dataSelectAfter?.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
{getInfoIcon('建议设置大于60s')} | |||
</FormItem> | |||
<FormItem | |||
label='计算类型' | |||
name={['dataFilter', 'type']} | |||
required={false} | |||
rules={[{ required: true, message: '请选择计算类型' }]} | |||
> | |||
<Select placeholder='请选择计算类型'> | |||
{computeType.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
</FormItem> | |||
<FormItem label='告警条件' required shouldUpdate style={{ marginBottom: 0 }}> | |||
{({ getFieldValue }) => ( | |||
<Fragment> | |||
<div style={{ display: 'inline-block', verticalAlign: '-webkit-baseline-middle', marginRight: 12 }}> | |||
过去 | |||
{getFieldValue(['dataFilter', 'range'])} | |||
{getLabel(getFieldValue(['dataFilter', 'rangeAfter']), FORM_CONFIG?.dataSelectAfter)} | |||
内的 | |||
{getLabel(getFieldValue(['dataFilter', 'type']), computeType)} | |||
</div> | |||
<FormItem | |||
name={['condition', 'operator']} | |||
style={{ display: 'inline-block', width: '120px' }} | |||
rules={[{ required: true, message: '请选择告警条件' }]} | |||
> | |||
<Select> | |||
{conditionOp.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
<FormItem | |||
name={['condition', 'value']} | |||
style={{ display: 'inline-block', width: '120px' }} | |||
rules={[{ required: true, message: '请输入告警条件' }]} | |||
> | |||
<InputNumber | |||
style={{ width: '100%' }} | |||
placeholder='请输入告警条件' | |||
min={0} | |||
max={100} | |||
formatter={value => `${value}%`} | |||
parser={value => Number((value as string).replace('%', ''))} | |||
/> | |||
</FormItem> | |||
</Fragment> | |||
)} | |||
</FormItem> | |||
<FormItem label='告警频率' required style={{ marginBottom: 0 }}> | |||
<FormItem | |||
name='frequency' | |||
style={{ display: 'inline-block', width: 'calc(100% - 80px)' }} | |||
rules={[ | |||
{ required: true, message: '请输入告警频率' }, | |||
{ pattern: /^[0-9]+$/, message: '请输入整数' }, | |||
]} | |||
> | |||
<Input placeholder='请输入整数,例如1' /> | |||
</FormItem> | |||
<FormItem | |||
name='frequencyAfter' | |||
style={{ display: 'inline-block', width: '80px' }} | |||
> | |||
<Select> | |||
{TIME_DATA.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
{getInfoIcon('告警频率是表示多长时间按告警条件去计算,看是否生成告警')} | |||
</FormItem> | |||
<FormItem label='触发通知阈值' required> | |||
<FormItem | |||
name='triggerCount' | |||
rules={[ | |||
{ required: true, message: '请输入触发通知阈值' }, | |||
{ pattern: /^[0-9]+$/, message: '请输入整数' }, | |||
]} | |||
noStyle | |||
> | |||
<Input placeholder='请输入整数,例如1' addonAfter={'次'} /> | |||
</FormItem> | |||
{getInfoIcon(`累计触发次数达到该值时,根据通知间隔发送告警。不满足触发条件时不计入统计。默认值为1,即满足一次触发条件就检查通知间隔。通过配置通知阈值可以实现多次触发,一次通知。例如,触发通知阈值为10,则累计触发次数达到10次时检查通知间隔,如果同时满足触发通知阈值和通知间隔,则发送通知。发送通知之后,累计次数会清零。如果因网络异常等原因执行检查失败,不计入累计次数。`)} | |||
</FormItem> | |||
<FormItem label='通知间隔' required style={{ marginBottom: 0 }}> | |||
<FormItem noStyle shouldUpdate> | |||
{({ getFieldValue }) => ( | |||
<FormItem | |||
name='interval' | |||
style={{ display: 'inline-block', width: 'calc(100% - 80px)' }} | |||
rules={[ | |||
{ required: getFieldValue('intervalAfter') !== 'now', message: '请输入通知间隔' }, | |||
{ pattern: /^[0-9]+$/, message: '请输入整数' }, | |||
]} | |||
> | |||
<Input placeholder={getFieldValue('intervalAfter') === 'now' ? '' : '请输入整数,例如1'} disabled={getFieldValue('intervalAfter') === 'now'} /> | |||
</FormItem> | |||
)} | |||
</FormItem> | |||
<FormItem | |||
name='intervalAfter' | |||
style={{ display: 'inline-block', width: '80px' }} | |||
> | |||
<Select onChange={onIntervalAfterChange}> | |||
{INTERVAL_TYPES.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
{getInfoIcon(`两次告警通知之间的时间间隔。如果某次查询符合触发条件,累计的触发次数达到触发通知阈值,且距离上次发送通知的时间已满足通知间隔,则发送通知。例如,通知间隔为10分钟,则10分钟内最多收到一次通知。通知间隔默认选立即,立即表示没有通知间隔,如果选立即,左边填的数字将清空并无法填写`)} | |||
</FormItem> | |||
<FormItem label='通知标题' required style={{ marginBottom: 0 }}> | |||
<FormItem | |||
name='level' | |||
style={{ display: 'inline-block', width: '80px' }} | |||
> | |||
<Select> | |||
{level.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
<FormItem | |||
name='title' | |||
style={{ display: 'inline-block', width: 'calc(100% - 80px)' }} | |||
rules={[ | |||
{ required: true, message: '请输入通知标题' }, | |||
// { pattern: /^[a-zA-Z_\u4e00-\u9fa5]+$/, message: '请输入中英文,最大不超过100个字符' }, | |||
]} | |||
> | |||
<Input placeholder='请输入通知标题,支持中英文,不超过100个字符' maxLength={100} /> | |||
</FormItem> | |||
</FormItem> | |||
<FormItem | |||
label='通知内容' | |||
name='content' | |||
rules={[{ required: true, message: '请输入通知内容' }]} | |||
> | |||
<TextArea placeholder='请输入通知内容' maxLength={2000} showCount /> | |||
</FormItem> | |||
<FormItem label='告警通道' required style={{ marginBottom: 0 }}> | |||
<Form.List name='receivers'> | |||
{(fields, { add, remove }) => { | |||
return ( | |||
<> | |||
{fields.map((field, index) => ( | |||
<FormItem required={false} key={field.key} style={{ marginBottom: 0 }}> | |||
<FormItem | |||
name={[field.name, 'hannel']} | |||
style={{ display: 'inline-block', width: '80px' }} | |||
> | |||
<Select> | |||
{receiverChannel.map(item => ( | |||
<Option key={item.value} value={item.value}>{item.label}</Option> | |||
))} | |||
</Select> | |||
</FormItem> | |||
<FormItem noStyle shouldUpdate> | |||
{({ getFieldValue }) => ( | |||
<FormItem | |||
name={[field.name, 'value']} | |||
style={{ display: 'inline-block', width: 'calc(100% - 80px)' }} | |||
rules={[ | |||
{ required: true, message: '请输入' }, | |||
getPatternRule(getFieldValue('receivers')[index].hannel), | |||
]} | |||
> | |||
<Input placeholder=' 请输入告警通知接收人邮箱地址' /> | |||
</FormItem> | |||
)} | |||
</FormItem> | |||
<Button className={styles.testEmail} type='primary' onClick={() => onTestEmailClick(field)}>测试发送</Button> | |||
{fields.length > 1 ? ( | |||
<MinusCircleOutlined onClick={() => remove(field.name)} className={styles.removeIcon} /> | |||
) : null} | |||
</FormItem> | |||
))} | |||
<FormItem> | |||
<Button | |||
type='dashed' | |||
onClick={() => add({ hannel: 'mail' })} | |||
style={{ width: '100%' }} | |||
icon={<PlusOutlined />} | |||
>添加告警通道</Button> | |||
</FormItem> | |||
</> | |||
) | |||
}} | |||
</Form.List> | |||
</FormItem> | |||
<FormItem wrapperCol={{ offset: 4, span: 16 }}> | |||
<Button type='primary' htmlType='submit' loading={loading}>{isEdit ? '保存' : '创建'}</Button> | |||
</FormItem> | |||
</Form> | |||
) | |||
} | |||
export default RuleForm; |
@@ -0,0 +1,22 @@ | |||
export const COMMON_LAYOUT = { | |||
labelCol: { span: 4 }, | |||
wrapperCol: { span: 16 }, | |||
}; | |||
export const TIME_DATA = [ | |||
{ label: '秒', value: 'second' }, | |||
{ label: '分', value: 'minute' }, | |||
{ label: '时', value: 'hour' }, | |||
{ label: '天', value: 'day' }, | |||
{ label: '周', value: 'week' }, | |||
] | |||
export const FORM_CONFIG = { | |||
dataLabel: '统计周期', | |||
dataSelectAfter: TIME_DATA, | |||
}; | |||