Browse Source

feat: add source management

v1.6.0
Yaofa.zhu 2 years ago
parent
commit
a650112ee0
36 changed files with 2214 additions and 51 deletions
  1. +6
    -1
      README.md
  2. +10
    -2
      config/proxy.js
  3. +45
    -16
      config/router.config.js
  4. +3
    -3
      package-lock.json
  5. +1
    -1
      package.json
  6. +12
    -7
      src/locales/zh-CN/menu.js
  7. +8
    -7
      src/models/common.ts
  8. +1
    -0
      src/models/connect.d.ts
  9. +0
    -0
      src/pages/Organization/AddAndEdit.less
  10. +1
    -1
      src/pages/Organization/AddAndEdit.tsx
  11. +0
    -0
      src/pages/Organization/index.less
  12. +2
    -2
      src/pages/Organization/index.tsx
  13. +0
    -0
      src/pages/Organization/services.ts
  14. +310
    -0
      src/pages/ResourceManagement/Alert/AlertHistory.tsx
  15. +200
    -0
      src/pages/ResourceManagement/Alert/AlertRules.tsx
  16. +168
    -0
      src/pages/ResourceManagement/Alert/CreatRule.tsx
  17. +492
    -0
      src/pages/ResourceManagement/Alert/_mock.js
  18. +73
    -0
      src/pages/ResourceManagement/Alert/components/HandleModal.tsx
  19. +66
    -0
      src/pages/ResourceManagement/Alert/components/RuleDetail.tsx
  20. +394
    -0
      src/pages/ResourceManagement/Alert/components/RuleForm.tsx
  21. +22
    -0
      src/pages/ResourceManagement/Alert/const.ts
  22. +59
    -0
      src/pages/ResourceManagement/Alert/index.less
  23. +64
    -0
      src/pages/ResourceManagement/Alert/index.tsx
  24. +89
    -0
      src/pages/ResourceManagement/Alert/model.ts
  25. +75
    -0
      src/pages/ResourceManagement/Alert/service.ts
  26. +24
    -0
      src/pages/ResourceManagement/Alert/util.ts
  27. +51
    -0
      src/pages/ResourceManagement/Monitor.tsx
  28. +10
    -0
      src/pages/ResourceManagement/locales/zh-CN.ts
  29. +4
    -0
      src/pages/ResourceManagement/locales/zh-CN/alert.ts
  30. +2
    -1
      src/pages/UserGroup/components/QuotaTable.tsx
  31. +4
    -3
      src/pages/UserGroup/create.tsx
  32. +1
    -2
      src/pages/UserGroup/list.tsx
  33. +9
    -3
      src/pages/UserGroup/service.ts
  34. +2
    -0
      src/utils/const.js
  35. +5
    -1
      src/utils/utils.js
  36. +1
    -1
      tsconfig.json

+ 6
- 1
README.md View File

@@ -1 +1,6 @@
# AIStudio ad-hub-frontend
# AIStudio apsc-frontend

```
npm i // 安装依赖(用yarn会有问题,所以 yarn.lock 文件可能会有依赖缺失)
npm/yarn start // 启动项目
```

+ 10
- 2
config/proxy.js View File

@@ -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,


+ 45
- 16
config/router.config.js View File

@@ -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: [


+ 3
- 3
package-lock.json View File

@@ -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",


+ 1
- 1
package.json View File

@@ -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",


+ 12
- 7
src/locales/zh-CN/menu.js View File

@@ -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': '编辑用户组资源',
};

+ 8
- 7
src/models/common.ts View File

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


+ 1
- 0
src/models/connect.d.ts View File

@@ -14,6 +14,7 @@ export interface UserStateType {
phone?: string,
email?: string,
currentVC: string[],
userType?: number,
},
}



src/pages/VirtualCluster/AddAndEdit.less → src/pages/Organization/AddAndEdit.less View File


src/pages/VirtualCluster/AddAndEdit.tsx → src/pages/Organization/AddAndEdit.tsx View File

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

src/pages/VirtualCluster/index.less → src/pages/Organization/index.less View File


src/pages/VirtualCluster/index.tsx → src/pages/Organization/index.tsx View File

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

src/pages/VirtualCluster/services.ts → src/pages/Organization/services.ts View File


+ 310
- 0
src/pages/ResourceManagement/Alert/AlertHistory.tsx View File

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

+ 200
- 0
src/pages/ResourceManagement/Alert/AlertRules.tsx View File

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

+ 168
- 0
src/pages/ResourceManagement/Alert/CreatRule.tsx View File

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

+ 492
- 0
src/pages/ResourceManagement/Alert/_mock.js View File

@@ -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,
}

+ 73
- 0
src/pages/ResourceManagement/Alert/components/HandleModal.tsx View File

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

+ 66
- 0
src/pages/ResourceManagement/Alert/components/RuleDetail.tsx View File

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

+ 394
- 0
src/pages/ResourceManagement/Alert/components/RuleForm.tsx View File

@@ -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 }}>
过去&nbsp;
{getFieldValue(['dataFilter', 'range'])}&nbsp;
{getLabel(getFieldValue(['dataFilter', 'rangeAfter']), FORM_CONFIG?.dataSelectAfter)}&nbsp;
内的&nbsp;
{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;

+ 22
- 0
src/pages/ResourceManagement/Alert/const.ts View File

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