🔨 前端升级.

This commit is contained in:
lijiahangmax
2025-09-24 15:54:01 +08:00
parent e67ee60361
commit 8aa8cda677
67 changed files with 5102 additions and 206 deletions

View File

@@ -0,0 +1,129 @@
import type { TableData } from '@arco-design/web-vue';
import type { DataGrid, OrderDirection, Pagination, ClearRequest } from '@/types/global';
import axios from 'axios';
import qs from 'query-string';
/**
* 告警记录处理请求
*/
export interface AlarmEventHandleRequest {
idList?: Array<number>;
handleStatus?: string;
handleTime?: number;
handleRemark?: string;
}
/**
* 告警记录误报请求
*/
export interface AlarmEventFalseAlarmRequest {
idList?: Array<number>;
}
/**
* 告警记录查询请求
*/
export interface AlarmEventQueryRequest extends Pagination, OrderDirection {
id?: number;
hostId?: number;
agentKey?: string;
policyId?: number;
metricsId?: number;
metricsMeasurement?: string;
alarmLevel?: number;
falseAlarm?: number;
handleStatus?: string;
handleRemark?: string;
handleUserId?: number;
createTimeRange?: string[];
}
/**
* 告警记录清理请求
*/
export interface AlarmEventClearRequest extends AlarmEventQueryRequest, ClearRequest {
}
/**
* 告警记录查询响应
*/
export interface AlarmEventQueryResponse extends TableData {
id: number;
hostId: number;
hostName: string;
hostAddress: string;
agentKey: string;
policyId: number;
metricsId: number;
metricsMeasurement: string;
alarmTags: string;
alarmValue: any;
alarmThreshold: any;
alarmInfo: string;
alarmLevel: number;
triggerCondition: string;
consecutiveCount: number;
falseAlarm: number;
handleStatus: string;
handleTime: number;
handleRemark: string;
handleUserId: number;
handleUsername: string;
createTime: number;
updateTime: number;
updater: string;
}
/**
* 处理告警记录
*/
export function handleAlarmEvent(request: AlarmEventHandleRequest) {
return axios.post<number>('/monitor/alarm-event/handle', request);
}
/**
* 设置为误报
*/
export function setAlarmEventFalse(request: AlarmEventFalseAlarmRequest) {
return axios.post<number>('/monitor/alarm-event/set-false', request);
}
/**
* 分页查询告警记录
*/
export function getAlarmEventPage(request: AlarmEventQueryRequest) {
return axios.post<DataGrid<AlarmEventQueryResponse>>('/monitor/alarm-event/query', request);
}
/**
* 查询告警记录数量
*/
export function getAlarmEventCount(request: AlarmEventQueryRequest) {
return axios.post<number>('/monitor/alarm-event/count', request);
}
/**
* 删除告警记录
*/
export function deleteAlarmEvent(id: number) {
return axios.delete<number>('/monitor/alarm-event/delete', { params: { id } });
}
/**
* 批量删除告警记录
*/
export function batchDeleteAlarmEvent(idList: Array<number>) {
return axios.delete<number>('/monitor/alarm-event/batch-delete', {
params: { idList },
paramsSerializer: params => {
return qs.stringify(params, { arrayFormat: 'comma' });
}
});
}
/**
* 清理告警记录
*/
export function clearMonitorAlarmEvent(request: AlarmEventClearRequest) {
return axios.post<number>('/monitor/alarm-event/clear', request);
}

View File

@@ -0,0 +1,92 @@
import type { TableData } from '@arco-design/web-vue';
import type { DataGrid, OrderDirection, Pagination } from '@/types/global';
import axios from 'axios';
/**
* 监控告警策略创建请求
*/
export interface AlarmPolicyCreateRequest {
name?: string;
description?: string;
notifyIdList?: Array<number>;
}
/**
* 监控告警策略更新请求
*/
export interface AlarmPolicyUpdateRequest extends AlarmPolicyCreateRequest {
id?: number;
updateNotify?: boolean;
}
/**
* 监控告警策略查询请求
*/
export interface AlarmPolicyQueryRequest extends Pagination, OrderDirection {
id?: number;
name?: string;
description?: string;
}
/**
* 监控告警策略查询响应
*/
export interface AlarmPolicyQueryResponse extends TableData {
id: number;
name: string;
description: string;
notifyIdList: Array<number>;
createTime: number;
updateTime: number;
creator: string;
updater: string;
}
/**
* 创建监控告警策略
*/
export function createAlarmPolicy(request: AlarmPolicyCreateRequest) {
return axios.post<number>('/monitor/alarm-policy/create', request);
}
/**
* 更新监控告警策略
*/
export function updateAlarmPolicy(request: AlarmPolicyUpdateRequest) {
return axios.put<number>('/monitor/alarm-policy/update', request);
}
/**
* 复制监控告警策略
*/
export function copyAlarmPolicy(request: AlarmPolicyCreateRequest) {
return axios.post<number>('/monitor/alarm-policy/copy', request);
}
/**
* 查询监控告警策略
*/
export function getAlarmPolicy(id: number) {
return axios.get<AlarmPolicyQueryResponse>('/monitor/alarm-policy/get', { params: { id } });
}
/**
* 查询全部监控告警策略
*/
export function getAlarmPolicyList() {
return axios.get<Array<AlarmPolicyQueryResponse>>('/monitor/alarm-policy/list');
}
/**
* 分页查询监控告警策略
*/
export function getAlarmPolicyPage(request: AlarmPolicyQueryRequest) {
return axios.post<DataGrid<AlarmPolicyQueryResponse>>('/monitor/alarm-policy/query', request);
}
/**
* 删除监控告警策略
*/
export function deleteAlarmPolicy(id: number) {
return axios.delete<number>('/monitor/alarm-policy/delete', { params: { id } });
}

View File

@@ -0,0 +1,84 @@
import type { TableData } from '@arco-design/web-vue';
import axios from 'axios';
/**
* 监控告警规则创建请求
*/
export interface AlarmRuleCreateRequest {
policyId?: number;
metricsId?: number;
tags?: string;
level?: number;
ruleSwitch?: number;
allEffect?: number;
triggerCondition?: string;
threshold?: any;
consecutiveCount?: number;
silencePeriod?: number;
description?: string;
}
/**
* 监控告警规则更新请求
*/
export interface AlarmRuleUpdateRequest extends AlarmRuleCreateRequest {
id?: number;
}
/**
* 监控告警规则查询响应
*/
export interface AlarmRuleQueryResponse extends TableData {
id: number;
policyId: number;
metricsId: number;
metricsMeasurement: string;
tags: string;
ruleSwitch: number;
allEffect: number;
level: number;
triggerCondition: string;
threshold: any;
consecutiveCount?: number;
silencePeriod: number;
description: string;
createTime: number;
updateTime: number;
creator: string;
updater: string;
}
/**
* 创建监控告警规则
*/
export function createAlarmRule(request: AlarmRuleCreateRequest) {
return axios.post<number>('/monitor/alarm-policy-rule/create', request);
}
/**
* 更新监控告警规则
*/
export function updateAlarmRule(request: AlarmRuleUpdateRequest) {
return axios.put<number>('/monitor/alarm-policy-rule/update', request);
}
/**
* 更新监控告警规则
*/
export function updateAlarmRuleSwitch(request: AlarmRuleUpdateRequest) {
return axios.put<number>('/monitor/alarm-policy-rule/update-switch', request);
}
/**
* 查询全部监控告警规则
*/
export function getAlarmRuleList(policyId: number, metricsMeasurement: string = '') {
return axios.get<Array<AlarmRuleQueryResponse>>('/monitor/alarm-policy-rule/list', { params: { policyId, metricsMeasurement } });
}
/**
* 删除监控告警规则
*/
export function deleteAlarmRule(id: number) {
return axios.delete<number>('/monitor/alarm-policy-rule/delete', { params: { id } });
}

View File

@@ -66,13 +66,6 @@ export function updateMetrics(request: MetricsUpdateRequest) {
return axios.put<number>('/monitor/monitor-metrics/update', request);
}
/**
* 查询监控指标
*/
export function getMetrics(id: number) {
return axios.get<MetricsQueryResponse>('/monitor/monitor-metrics/get', { params: { id } });
}
/**
* 查询全部监控指标
*/

View File

@@ -20,7 +20,7 @@ export interface MonitorHostUpdateRequest {
* 监控主机更新请求
*/
export interface MonitorHostSwitchUpdateRequest {
id?: number;
idList?: Array<number>;
alarmSwitch?: number;
}
@@ -57,6 +57,23 @@ export interface MonitorHostChartRequest {
end?: string;
}
/**
* 监控指标数据
*/
export interface MonitorMetricsData {
timestamp: number;
metrics: Array<MonitorMetrics>;
}
/**
* 监控指标
*/
export interface MonitorMetrics {
type: string;
tags: Record<string, string>;
values: Record<string, number>;
}
/**
* 监控主机查询响应
*/
@@ -139,6 +156,13 @@ export function getMonitorHostMetrics(agentKeyList: Array<string>) {
});
}
/**
* 获取监控主机概览
*/
export function getMonitorHostOverride(agentKey: string) {
return axios.get<MonitorMetricsData>('/monitor/monitor-host/override', { params: { agentKey } });
}
/**
* 查询监控主机图表
*/

View File

@@ -0,0 +1,91 @@
import type { TableData } from '@arco-design/web-vue';
import type { DataGrid, OrderDirection, Pagination } from '@/types/global';
import axios from 'axios';
/**
* 通知模板创建请求
*/
export interface NotifyTemplateCreateRequest {
name?: string;
bizType?: string;
channelType?: string;
channelConfig?: string;
messageTemplate?: string;
description?: string;
}
/**
* 通知模板更新请求
*/
export interface NotifyTemplateUpdateRequest extends NotifyTemplateCreateRequest {
id?: number;
}
/**
* 通知模板查询请求
*/
export interface NotifyTemplateQueryRequest extends Pagination, OrderDirection {
id?: number;
name?: string;
bizType?: string;
channelType?: string;
}
/**
* 通知模板查询响应
*/
export interface NotifyTemplateQueryResponse extends TableData {
id: number;
name: string;
bizType: string;
channelType: string;
channelConfig: string;
messageTemplate: string;
description: string;
createTime: number;
updateTime: number;
creator: string;
updater: string;
}
/**
* 创建通知模板
*/
export function createNotifyTemplate(request: NotifyTemplateCreateRequest) {
return axios.post<number>('/infra/notify-template/create', request);
}
/**
* 更新通知模板
*/
export function updateNotifyTemplate(request: NotifyTemplateUpdateRequest) {
return axios.put<number>('/infra/notify-template/update', request);
}
/**
* 查询通知模板
*/
export function getNotifyTemplate(id: number) {
return axios.get<NotifyTemplateQueryResponse>('/infra/notify-template/get', { params: { id } });
}
/**
* 查询全部通知模板
*/
export function getNotifyTemplateList(bizType: string) {
return axios.get<Array<NotifyTemplateQueryResponse>>('/infra/notify-template/list', { params: { bizType } });
}
/**
* 分页查询通知模板
*/
export function getNotifyTemplatePage(request: NotifyTemplateQueryRequest) {
return axios.post<DataGrid<NotifyTemplateQueryResponse>>('/infra/notify-template/query', request);
}
/**
* 删除通知模板
*/
export function deleteNotifyTemplate(id: number) {
return axios.delete<number>('/infra/notify-template/delete', { params: { id } });
}

View File

@@ -16,7 +16,7 @@ export interface OperatorLogQueryRequest extends Pagination, OrderDirection {
}
/**
* 操作日志清理参数
* 操作日志清空请求
*/
export interface OperatorLogClearRequest extends OperatorLogQueryRequest, ClearRequest {
}

View File

@@ -0,0 +1,54 @@
<template>
<a-select v-model:model-value="modelValue"
:options="optionData"
:allow-search="true"
:loading="loading"
:disabled="loading"
placeholder="请选择告警策略" />
</template>
<script lang="ts">
export default {
name: 'alarmPolicySelector'
};
</script>
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import { onActivated, onMounted, ref } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
const modelValue = defineModel({ type: Number });
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const optionData = ref<Array<SelectOptionData>>([]);
// 初始化选项
const initOptions = async () => {
setLoading(true);
try {
const values = await cacheStore.loadMonitorAlarmPolicy();
optionData.value = values.map(s => {
return {
label: s.name,
value: s.id,
};
});
} catch (e) {
} finally {
setLoading(false);
}
};
// 初始化选项
onMounted(initOptions);
onActivated(initOptions);
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,54 @@
<template>
<a-select v-model:model-value="modelValue"
:options="optionData"
:allow-search="true"
:loading="loading"
:disabled="loading"
placeholder="请选择监控指标" />
</template>
<script lang="ts">
export default {
name: 'monitorMetricsSelector'
};
</script>
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import { onActivated, onMounted, ref } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
const modelValue = defineModel({ type: Number });
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const optionData = ref<Array<SelectOptionData>>([]);
// 初始化选项
const initOptions = async () => {
setLoading(true);
try {
const values = await cacheStore.loadMonitorMetricsList();
optionData.value = values.map(s => {
return {
label: s.name,
value: s.id,
};
});
} catch (e) {
} finally {
setLoading(false);
}
};
// 初始化选项
onMounted(initOptions);
onActivated(initOptions);
</script>
<style lang="less" scoped>
</style>

View File

@@ -22,25 +22,25 @@
<a-space>
<!-- 状态 -->
<a-switch v-model="queryUnread"
style="margin-right: 4px;"
type="round"
checked-text="未读"
unchecked-text="全部"
@change="reloadAllMessage" />
<!-- 清空 -->
<a-button class="header-button"
type="text"
size="small"
title="清空全部已读消息"
@click="clearAllMessage">
清空
</a-button>
<!-- 全部已读 -->
<a-button class="header-button"
type="text"
size="small"
@click="setAllRead">
全部已读
</a-button>
<!-- 更多操作 -->
<a-dropdown trigger="hover" :popup-max-height="false">
<icon-more class="card-extra-icon" />
<template #content>
<!-- 全部已读 -->
<a-doption title="=全部已读" @click="setAllRead">
<span class="more-doption normal">全部已读</span>
</a-doption>
<!-- 清空已读 -->
<a-doption title="清空全部已读消息" @click="clearAllMessage">
<span class="more-doption normal">清空已读</span>
</a-doption>
</template>
</a-dropdown>
</a-space>
</template>
</a-tabs>
@@ -263,6 +263,10 @@
.header-button {
padding: 0 6px;
}
:deep(.arco-tabs-tab) {
margin: 0 6px 0 0 !important;
}
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<a-select v-model:model-value="modelValue"
:options="optionData"
:allow-search="true"
:multiple="multiple"
:loading="loading"
:disabled="loading"
placeholder="请选择通知模板" />
</template>
<script lang="ts">
export default {
name: 'notifyTemplateSelector'
};
</script>
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import { onActivated, onMounted, ref } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
const props = withDefaults(defineProps<{
multiple?: boolean;
bizType: string;
}>(), {
multiple: false,
});
const modelValue = defineModel({ type: Array<number> });
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const optionData = ref<Array<SelectOptionData>>([]);
// 初始化选项
const initOptions = async () => {
setLoading(true);
try {
const values = await cacheStore.loadNotifyTemplate(props.bizType);
optionData.value = values.map(s => {
return {
label: s.name,
value: s.id,
};
});
} catch (e) {
} finally {
setLoading(false);
}
};
// 初始化选项
onMounted(initOptions);
onActivated(initOptions);
</script>
<style lang="less" scoped>
</style>

View File

@@ -17,6 +17,16 @@ const MONITOR: AppRouteRecordRaw = {
path: '/monitor/monitor-host',
component: () => import('@/views/monitor/monitor-host/index.vue'),
},
{
name: 'alarmPolicy',
path: '/monitor/alarm-policy',
component: () => import('@/views/monitor/alarm-policy/index.vue'),
},
{
name: 'alarmEvent',
path: '/monitor/alarm-event',
component: () => import('@/views/monitor/alarm-event/index.vue'),
},
{
name: 'monitorDetail',
path: '/monitor/detail',
@@ -32,6 +42,21 @@ const MONITOR: AppRouteRecordRaw = {
},
component: () => import('@/views/monitor/monitor-detail/index.vue'),
},
{
name: 'alarmRule',
path: '/monitor/alarm-rule',
meta: {
// 固定到 tab
noAffix: false,
// 是否允许打开多个 tab
multipleTab: true,
// 名称模板
localeTemplate: (route: RouteLocationNormalized) => {
return `${route.meta.locale} - ${route.query.name || ''}`;
},
},
component: () => import('@/views/monitor/alarm-rule/index.vue'),
},
],
};

View File

@@ -21,6 +21,11 @@ const SYSTEM: AppRouteRecordRaw = {
path: '/system/dict-value',
component: () => import('@/views/system/dict-value/index.vue'),
},
{
name: 'notifyTemplate',
path: '/system/notify-template',
component: () => import('@/views/system/notify-template/index.vue'),
},
{
name: 'systemSetting',
path: '/system/setting',

View File

@@ -24,6 +24,9 @@ import { getExecJobList } from '@/api/exec/exec-job';
import { getPathBookmarkGroupList } from '@/api/terminal/path-bookmark-group';
import { getCommandSnippetList } from '@/api/terminal/command-snippet';
import { getPathBookmarkList } from '@/api/terminal/path-bookmark';
import { getNotifyTemplateList } from '@/api/system/notify-template';
import { getAlarmPolicyList } from '@/api/monitor/alarm-policy';
import { getMetricsList } from '@/api/monitor/metrics';
export default defineStore('cache', {
state: (): CacheState => ({}),
@@ -173,6 +176,21 @@ export default defineStore('cache', {
return await this.load('execJob', getExecJobList, ['exec:exec-job:query'], force);
},
// 查询监控告警策略列表
async loadMonitorAlarmPolicy(force = false) {
return await this.load('alarmPolicy', getAlarmPolicyList, ['monitor:alarm-policy:query'], force);
},
// 查询监控指标列表
async loadMonitorMetricsList(force = false) {
return await this.load('monitorMetrics', getMetricsList, ['monitor:monitor-metrics:query'], force);
},
// 查询通知模板列表
async loadNotifyTemplate(bizType: string, force = false) {
return await this.load(`notifyTemplate_${bizType}`, () => getNotifyTemplateList(bizType), ['infra:notify-template:query'], force);
},
// 加载偏好
async loadPreference<T>(type: PreferenceType, force = false) {
return await this.load(`preference_${type}`, () => getPreference<T>(type), undefined, force, {});
@@ -185,8 +203,8 @@ export default defineStore('cache', {
// 加载系统设置
async loadSystemSetting(force = false) {
return await this.load(`system_setting`, getSystemAggregateSetting, undefined, force, {});
},
return await this.load('systemSetting', getSystemAggregateSetting, undefined, force, {});
}
}
});

View File

@@ -7,10 +7,11 @@ export type CacheType = 'users' | 'menus' | 'roles'
| 'authorizedHostKeys' | 'authorizedHostIdentities'
| 'commandSnippetGroups' | 'pathBookmarkGroups'
| 'commandSnippets' | 'pathBookmarks'
| 'system_setting'
| 'alarmPolicy' | 'monitorMetrics'
| 'systemSetting' | 'notifyTemplate*'
| '*_Tags' | 'preference_*'
| string
export interface CacheState {
[key: CacheType]: unknown;
[key: CacheType]: any;
}

View File

@@ -1,5 +1,8 @@
import type { SelectOptionData, TreeNodeData } from '@arco-design/web-vue';
// 表单操作
export type FormHandle = 'add' | 'update' | 'copy' | 'view';
// 通过 label 进行过滤
export const labelFilter = (searchValue: string, option: { label: string }) => {
return option.label.toLowerCase().includes(searchValue.toLowerCase());

View File

@@ -1,10 +1,14 @@
// 获取百分比进度状态
export const getPercentProgressColor = (percent: number) => {
if (percent < 0.6) {
return 'rgb(var(--green-6))';
} else if (percent < 0.8) {
return 'rgb(var(--orange-6))';
} else {
return 'rgb(var(--red-6))';
// 获取百分比进度颜色
export const getPercentProgressColor = (percent: number, defaultColor = 'rgb(var(--green-6))') => {
try {
if (percent < 0.6) {
return defaultColor;
} else if (percent < 0.8) {
return 'rgb(var(--orange-6))';
} else {
return 'rgb(var(--red-6))';
}
} catch (e) {
return defaultColor;
}
};

View File

@@ -239,6 +239,10 @@ export function replaceHtmlTag(message: string) {
.replaceAll('&lt;sr 2&gt;', '<span class="span-red mx2">')
.replaceAll('&lt;sr 4&gt;', '<span class="span-red mx4">')
.replaceAll('&lt;/sr&gt;', '</span>')
.replaceAll('&lt;sg&gt;', '<span class="span-green mx0">')
.replaceAll('&lt;sg 2&gt;', '<span class="span-green mx2">')
.replaceAll('&lt;sg 4&gt;', '<span class="span-green mx4">')
.replaceAll('&lt;/sg&gt;', '</span>')
.replaceAll('&lt;b&gt;', '<b>')
.replaceAll('&lt;/b&gt;', '</b>');
}
@@ -256,9 +260,24 @@ export function clearHtmlTag(message: string) {
.replaceAll('&lt;sr 2&gt;', '')
.replaceAll('&lt;sr&gt;', '')
.replaceAll('&lt;/sr&gt;', '')
.replaceAll('&lt;sg 0&gt;', '')
.replaceAll('&lt;sg 2&gt;', '')
.replaceAll('&lt;sg&gt;', '')
.replaceAll('&lt;/sg&gt;', '')
.replaceAll('&lt;b&gt;', '')
.replaceAll('&lt;/b&gt;', '')
.replaceAll('<br/>', '\n');
}
/**
* 分配记录 (忽略基础信息)
*/
export const assignOmitRecord = (record: any, ...omits: Array<string>) => {
const model = Object.assign({}, record);
for (const omitKey of [...omits, 'creator', 'updater', 'createTime', 'updateTime']) {
delete model[omitKey];
}
return model;
};
export default null;

View File

@@ -27,7 +27,7 @@ export type WindowUnit =
// 指标单位格式化选项
export interface MetricUnitFormatOptions {
// 小数位
digit?: number;
precision?: number;
// 后缀
suffix?: string;
// 空转0
@@ -37,7 +37,12 @@ export interface MetricUnitFormatOptions {
}
// 指标单位格式化函数
type MetricUnitFormatterFn = (value: number, option?: MetricUnitFormatOptions) => string;
type MetricUnitFormatterOption = {
// 格式化单位
format: (value: number, option?: MetricUnitFormatOptions) => string;
// 获取阈值原始值
getThresholdOriginalValue: (value: number) => number;
};
// 指标单位格式化配置
type WindowTimeFormatterOption = {
@@ -54,27 +59,57 @@ type WindowTimeFormatterOption = {
};
// 指标单位格式化
export const MetricUnitFormatter: Record<MetricUnitType, MetricUnitFormatterFn> = {
export const MetricUnitFormatter: Record<MetricUnitType, MetricUnitFormatterOption> = {
// 字节
BYTES: formatBytes,
BYTES: {
format: formatBytes,
getThresholdOriginalValue: getByteThresholdOriginalValue,
},
// 比特
BITS: formatBits,
BITS: {
format: formatBits,
getThresholdOriginalValue: getBitThresholdOriginalValue,
},
// 次数
COUNT: formatCount,
COUNT: {
format: formatCount,
getThresholdOriginalValue: identity,
},
// 秒
SECONDS: formatSeconds,
SECONDS: {
format: formatSeconds,
getThresholdOriginalValue: identity,
},
// 百分比
PER: formatPer,
PER: {
format: formatPer,
getThresholdOriginalValue: identity,
},
// 字节/秒
BYTES_S: (value, option) => formatBytes(value, option) + '/s',
BYTES_S: {
format: (value, option) => formatBytes(value, option) + '/s',
getThresholdOriginalValue: getByteThresholdOriginalValue,
},
// 比特/秒
BITS_S: (value, option) => formatBits(value, option) + 'ps',
BITS_S: {
format: (value, option) => formatBits(value, option) + 'ps',
getThresholdOriginalValue: getBitThresholdOriginalValue,
},
// 次数/秒
COUNT_S: (value, option) => formatCount(value, option) + '/s',
COUNT_S: {
format: (value, option) => formatCount(value, option) + '/s',
getThresholdOriginalValue: identity,
},
// 文本
TEXT: formatText,
TEXT: {
format: formatText,
getThresholdOriginalValue: identity,
},
// 无单位
NONE: (value, option) => formatNumber(value, option),
NONE: {
format: formatText,
getThresholdOriginalValue: identity,
},
};
// 窗口单位格式化
@@ -124,39 +159,26 @@ export const parseWindowUnit = (windowValue: string): [number, WindowUnit] => {
}
};
// 安全取小数
function getFixed(option?: MetricUnitFormatOptions, defaultValue = 2): number {
return typeof option?.digit === 'number' ? option.digit : defaultValue;
// 提取单
export function extractUnit(str: string): string {
const match = str.match(/[^\d.]+$/);
return match ? match[0] : '';
}
// 格式化数字
function formatNumber(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getFixed(option, 2);
const abs = Math.abs(value);
let result: string;
if (abs >= 1e9) {
result = (value / 1e9).toFixed(fixed);
} else if (abs >= 1_000_000) {
result = (value / 1_000_000).toFixed(fixed);
} else if (abs >= 1_000) {
result = (value / 1_000).toFixed(fixed);
} else {
result = value.toFixed(fixed);
}
return parseFloat(result).toString();
// 安全取小数位
function getPrecision(option?: MetricUnitFormatOptions, defaultValue = 2): number {
return typeof option?.precision === 'number' ? option.precision : defaultValue;
}
// 格式化百分比
function formatPer(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getFixed(option, 2);
const fixed = getPrecision(option, 2);
return parseFloat((value).toFixed(fixed)) + '%';
}
// 格式化字节
function formatBytes(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getFixed(option, 2);
export function formatBytes(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getPrecision(option, 2);
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let v = Math.abs(value);
let i = 0;
@@ -170,10 +192,10 @@ function formatBytes(value: number, option?: MetricUnitFormatOptions): string {
}
// 格式化比特
function formatBits(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getFixed(option, 2);
export function formatBits(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getPrecision(option, 2);
const units = ['b', 'Kb', 'Mb', 'Gb'];
let v = Math.abs(value);
let v = Math.abs(value * 8);
let i = 0;
while (v >= 1000 && i < units.length - 1) {
v /= 1000;
@@ -186,7 +208,7 @@ function formatBits(value: number, option?: MetricUnitFormatOptions): string {
// 格式化次数
function formatCount(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getFixed(option, 2);
const fixed = getPrecision(option, 2);
const abs = Math.abs(value);
if (abs >= 1_000_000) {
return parseFloat((value / 1_000_000).toFixed(fixed)) + 'M';
@@ -198,7 +220,7 @@ function formatCount(value: number, option?: MetricUnitFormatOptions): string {
// 格式化时间
function formatSeconds(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getFixed(option, 2);
const fixed = getPrecision(option, 2);
if (value >= 3600) {
return parseFloat((value / 3600).toFixed(fixed)) + 'h';
} else if (value >= 60) {
@@ -209,8 +231,23 @@ function formatSeconds(value: number, option?: MetricUnitFormatOptions): string
// 格式化文本
function formatText(value: number, option?: MetricUnitFormatOptions): string {
const fixed = getFixed(option, 2);
const fixed = getPrecision(option, 2);
const unitText = option?.suffix || '';
const numStr = value.toFixed(fixed);
return unitText ? `${numStr} ${unitText}` : numStr;
}
// 获取 byte 的阈值原值 MB > b
function getByteThresholdOriginalValue(value: number) {
return value * 1024 * 1024;
}
// 获取 bit 的阈值原值 Mb > bit
function getBitThresholdOriginalValue(value: number) {
return value / 8 * 1000 * 1000;
}
// 返回原值
function identity(value: number): number {
return value;
}

View File

@@ -22,6 +22,7 @@ const columns = [
title: '模板命令',
dataIndex: 'command',
slotName: 'command',
minWidth: 380,
align: 'left',
ellipsis: true,
default: true,

View File

@@ -0,0 +1,223 @@
<template>
<a-modal v-model:visible="visible"
modal-class="modal-form-large"
title-align="start"
title="清理操作日志"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
ok-text="清理"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-form :model="formModel"
label-align="right"
:auto-label-width="true">
<!-- 处理状态 -->
<a-form-item field="handleStatus" label="处理状态">
<a-select v-model="formModel.handleStatus"
:options="toOptions(HandleStatusKey)"
placeholder="请选择处理状态"
allow-clear />
</a-form-item>
<!-- 告警主机 -->
<a-form-item field="hostId" label="告警主机">
<host-selector v-model="formModel.hostId"
placeholder="请选择告警主机"
hide-button
allow-clear />
</a-form-item>
<!-- 告警级别 -->
<a-form-item field="alarmLevel" label="告警级别">
<a-select v-model="formModel.alarmLevel"
:options="toOptions(AlarmLevelKey)"
placeholder="请选择告警级别"
allow-clear />
</a-form-item>
<!-- 处理人 -->
<a-form-item field="handleUserId" label="处理人">
<user-selector v-model="formModel.handleUserId"
placeholder="请选择处理人"
hide-button
allow-clear />
</a-form-item>
<!-- 处理备注 -->
<a-form-item field="handleRemark" label="处理备注">
<a-input v-model="formModel.handleRemark"
placeholder="请输入处理备注"
allow-clear />
</a-form-item>
<!-- 告警策略 -->
<a-form-item field="policyId" label="告警策略">
<alarm-policy-selector v-model="formModel.policyId"
placeholder="请输入告警策略"
hide-button
allow-clear />
</a-form-item>
<!-- 数据集 -->
<a-form-item field="metricsId" label="数据集">
<a-select v-model="formModel.metricsMeasurement"
:options="toOptions(MetricsMeasurementKey)"
placeholder="数据集"
allow-clear />
</a-form-item>
<!-- 告警指标 -->
<a-form-item field="metricsId" label="告警指标">
<monitor-metrics-selector v-model="formModel.metricsId"
placeholder="请选择告警指标"
hide-button
allow-clear />
</a-form-item>
<!-- 是否误报 -->
<a-form-item field="falseAlarm" label="是否误报">
<a-select v-model="formModel.falseAlarm"
:options="toOptions(FalseAlarmKey)"
placeholder="请选择是否误报"
allow-clear />
</a-form-item>
<!-- id -->
<a-form-item field="id" label="id">
<a-input-number v-model="formModel.id"
placeholder="请输入id"
hide-button
allow-clear />
</a-form-item>
<!-- agentKey -->
<a-form-item field="agentKey" label="agentKey">
<a-input v-model="formModel.agentKey"
placeholder="请输入agentKey"
allow-clear />
</a-form-item>
<!-- 告警时间 -->
<a-form-item field="createTimeRange" label="告警时间">
<a-range-picker v-model="formModel.createTimeRange"
style="width: 100%;"
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
show-time
format="YYYY-MM-DD HH:mm:ss" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'alarmEventClearModal'
};
</script>
<script lang="ts" setup>
import type { AlarmEventQueryRequest } from '@/api/monitor/alarm-event';
import { clearMonitorAlarmEvent, getAlarmEventCount } from '@/api/monitor/alarm-event';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { Message, Modal } from '@arco-design/web-vue';
import { useDictStore } from '@/store';
import { maxClearLimit, HandleStatusKey, AlarmLevelKey, MetricsMeasurementKey, FalseAlarmKey } from '../types/const';
import { assignOmitRecord } from '@/utils';
import UserSelector from '@/components/user/user/selector/index.vue';
import HostSelector from '@/components/asset/host/selector/index.vue';
import MonitorMetricsSelector from '@/components/monitor/metrics/selector/index.vue';
import AlarmPolicySelector from '@/components/monitor/alarm-policy/selector/index.vue';
const { toOptions } = useDictStore();
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const defaultForm = (): AlarmEventQueryRequest => {
return {
id: undefined,
hostId: undefined,
agentKey: undefined,
policyId: undefined,
metricsId: undefined,
metricsMeasurement: undefined,
alarmLevel: undefined,
falseAlarm: undefined,
handleStatus: undefined,
handleRemark: undefined,
handleUserId: undefined,
createTimeRange: undefined,
limit: maxClearLimit,
};
};
const formModel = ref<AlarmEventQueryRequest>({});
const emits = defineEmits(['clear']);
// 打开
const open = (record: AlarmEventQueryRequest) => {
formModel.value = assignOmitRecord({ ...defaultForm(), ...record });
setVisible(true);
};
defineExpose({ open });
// 确定
const handlerOk = async () => {
if (!formModel.value.limit) {
Message.error('请输入数量限制');
return false;
}
setLoading(true);
try {
// 获取总数量
const { data } = await getAlarmEventCount(formModel.value);
if (data) {
// 清空
doClear(data);
} else {
// 无数据
Message.warning('当前条件未查询到数据');
}
} catch (e) {
} finally {
setLoading(false);
}
return false;
};
// 执行删除
const doClear = (count: number) => {
Modal.confirm({
title: '删除清空',
content: `确定要删除 ${count} 条数据吗? 确定后将立即删除且无法恢复!`,
onOk: async () => {
setLoading(true);
try {
// 调用清空
const { data } = await clearMonitorAlarmEvent(formModel.value);
Message.success(`已成功清空 ${data} 条数据`);
emits('clear');
// 清空
setVisible(false);
handlerClear();
} catch (e) {
} finally {
setLoading(false);
}
}
});
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,130 @@
<template>
<a-modal v-model:visible="visible"
modal-class="modal-form-large"
title-align="start"
title="处理告警"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
ok-text="处理"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-form ref="formRef"
:model="formModel"
label-align="right"
:rules="handleRules"
:auto-label-width="true">
<!-- 处理状态 -->
<a-form-item field="handleStatus" label="处理状态">
<a-select v-model="formModel.handleStatus"
:options="toOptions(HandleStatusKey)"
placeholder="请选择处理状态"
allow-clear />
</a-form-item>
<!-- 处理时间 -->
<a-form-item field="handleTime" label="处理时间">
<a-date-picker v-model="formModel.handleTime"
style="width: 100%"
placeholder="请选择处理时间"
show-time
allow-clear />
</a-form-item>
<!-- 处理备注 -->
<a-form-item field="handleRemark" label="处理备注">
<a-textarea v-model="formModel.handleRemark"
:auto-size="{ minRows: 4, maxRows: 4 }"
placeholder="请输入处理备注"
allow-clear />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'alarmEventHandleModal'
};
</script>
<script lang="ts" setup>
import type { AlarmEventHandleRequest } from '@/api/monitor/alarm-event';
import { ref } from 'vue';
import { handleAlarmEvent } from '@/api/monitor/alarm-event';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { Message } from '@arco-design/web-vue';
import { useDictStore } from '@/store';
import { HandleStatusKey } from '../types/const';
import { assignOmitRecord } from '@/utils';
import { handleRules } from '../types/form.rules';
const { toOptions } = useDictStore();
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const defaultForm = (): AlarmEventHandleRequest => {
return {
idList: undefined,
handleStatus: undefined,
handleRemark: undefined,
handleTime: Date.now(),
};
};
const formRef = ref();
const formModel = ref<AlarmEventHandleRequest>({});
const emits = defineEmits(['handled']);
// 打开
const open = (idList: Array<number>) => {
formModel.value = assignOmitRecord({ ...defaultForm(), idList });
setVisible(true);
};
defineExpose({ open });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
// 处理
await handleAlarmEvent(formModel.value);
Message.success('已处理');
emits('handled', { ...formModel.value });
// 清空
handlerClear();
return true;
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
setVisible(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,348 @@
<template>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧操作 -->
<div class="table-left-bar-handle">
<!-- 标题 -->
<div class="table-title">
告警记录列表
</div>
</div>
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 清理 -->
<a-button v-if="showClearButton"
v-permission="['monitor:alarm-event:management:clear']"
status="danger"
@click="$emit('openClear', formModel)">
清理
<template #icon>
<icon-close />
</template>
</a-button>
<!-- 处理告警 -->
<a-button v-permission="['monitor:alarm-event:handle']"
type="primary"
:disabled="selectedKeys.length === 0"
@click="$emit('openHandle', selectedKeys)">
处理告警
<template #icon>
<icon-play-arrow-fill />
</template>
</a-button>
<!-- 标记误报 -->
<a-button v-permission="['monitor:alarm-event:handle']"
type="primary"
:disabled="selectedKeys.length === 0"
@click="setFalseAlarm(selectedKeys, true)">
标记误报
<template #icon>
<icon-bug />
</template>
</a-button>
<!-- 删除 -->
<a-button v-permission="['monitor:alarm-event:delete']"
type="secondary"
status="danger"
:disabled="selectedKeys.length === 0"
@click="deleteRows(selectedKeys)">
删除
<template #icon>
<icon-delete />
</template>
</a-button>
<!-- 调整 -->
<table-adjust :columns="columns"
:columns-hook="columnsHook"
:query-order="queryOrder"
@query="$emit('query')" />
</a-space>
</div>
</template>
<!-- table -->
<a-table v-model:selected-keys="selectedKeys"
row-key="id"
ref="tableRef"
:loading="loading"
:columns="tableColumns"
:row-selection="rowSelection"
:data="tableData"
:pagination="pagination"
:bordered="false"
:scroll="{ x: 'auto' }"
@page-change="(page: number) => $emit('query', page, pagination.pageSize)"
@page-size-change="(size: number) => $emit('query', 1, size)">
<!-- 主机信息 -->
<template #hostInfo="{ record }">
<div class="info-wrapper">
<div class="info-item">
<span class="info-label">主机名称</span>
<span class="info-value text-copy text-ellipsis"
:title="record.hostName"
@click="copy(record.hostName, true)">
{{ record.hostName }}
</span>
</div>
<div class="info-item">
<span class="info-label">主机地址</span>
<span class="info-value span-blue text-copy text-ellipsis"
:title="record.hostAddress"
@click="copy(record.hostAddress, true)">
{{ record.hostAddress }}
</span>
</div>
</div>
</template>
<!-- 处理状态 -->
<template #handleStatus="{ record }">
<!-- 是否误报 -->
<a-tag v-if="record.falseAlarm === FalseAlarm.TRUE" color="arcoblue">
{{ getDictValue(FalseAlarmKey, record.falseAlarm) }}
</a-tag>
<!-- 处理状态 -->
<a-tag v-else :color="getDictValue(HandleStatusKey, record.handleStatus, 'color')">
{{ getDictValue(HandleStatusKey, record.handleStatus) }}
</a-tag>
</template>
<!-- 告警级别 -->
<template #alarmLevel="{ record }">
<a-tag :color="getDictValue(AlarmLevelKey, record.alarmLevel, 'color')">
{{ getDictValue(AlarmLevelKey, record.alarmLevel) }}
</a-tag>
</template>
<!-- 指标数据集 -->
<template #metricsMeasurement="{ record }">
{{ getDictValue(MetricsMeasurementKey, record.metricsMeasurement) }}
</template>
<!-- 告警指标 -->
<template #metricsId="{ record }">
<div>
<b class="span-blue">{{ getMetricsField(record.metricsId, 'value') }}</b>
<br />
<b>{{ getMetricsField(record.metricsId, 'name') }}</b>
</div>
</template>
<!-- 告警标签 -->
<template #alarmTags="{ record }">
<component :is="extraTags(record)" />
</template>
<!-- 告警值 -->
<template #alarmValue="{ record }">
<b class="span-red">{{ formatMetricsValueUnit(record.alarmValue, record) }}</b>
</template>
<!-- 告警阈值 -->
<template #alarmThreshold="{ record }">
<b class="span-red">{{ getDictValue(TriggerConditionKey, record.triggerCondition) }} {{ formatMetricsValueUnit(record.alarmThreshold, record) }}</b>
</template>
<!-- 持续数据点 -->
<template #consecutiveCount="{ record }">
{{ record.consecutiveCount }} 次
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 处理 -->
<a-button v-permission="['monitor:alarm-event:handle']"
type="text"
@click="$emit('openHandle', [record.id])">
处理
</a-button>
<!-- 更多 -->
<a-dropdown trigger="hover" :popup-max-height="false">
<a-button type="text" size="mini">
更多
</a-button>
<template #content>
<!-- 标记误报 -->
<a-doption v-permission="['monitor:alarm-event:handle']"
@click="setFalseAlarm([record.id], false)">
<span class="more-doption normal">标记误报</span>
</a-doption>
<!-- 删除 -->
<a-doption v-permission="['monitor:alarm-event:delete']"
@click="deleteRows([record.id])">
<span class="more-doption error">删除</span>
</a-doption>
</template>
</a-dropdown>
</div>
</template>
</a-table>
</a-card>
</template>
<script lang="ts">
export default {
name: 'alarmEventTableBase'
};
</script>
<script lang="ts" setup>
import type { MetricsQueryResponse } from '@/api/monitor/metrics';
import type { AlarmEventQueryRequest, AlarmEventQueryResponse, AlarmEventHandleRequest } from '@/api/monitor/alarm-event';
import { h, ref } from 'vue';
import { batchDeleteAlarmEvent, setAlarmEventFalse } from '@/api/monitor/alarm-event';
import { Message, Modal, Space, Tag, type PaginationProps } from '@arco-design/web-vue';
import {
FalseAlarm,
HandleStatusKey,
FalseAlarmKey,
MetricsMeasurementKey,
AlarmLevelKey,
TriggerConditionKey
} from '@/views/monitor/alarm-event/types/const';
import { useRowSelection, useTableColumns } from '@/hooks/table';
import { copy } from '@/hooks/copy';
import { useQueryOrder, DESC } from '@/hooks/query-order';
import { useDictStore, useCacheStore, useUserStore } from '@/store';
import { MetricsUnit, MetricUnitFormatter } from '@/utils/metrics';
import TableAdjust from '@/components/app/table-adjust/index.vue';
const props = defineProps<{
tableName: string;
columns: any[];
tableData: AlarmEventQueryResponse[];
loading: boolean;
formModel: AlarmEventQueryRequest;
pagination: PaginationProps;
showClearButton?: boolean;
}>();
const emits = defineEmits<{
openHandle: [ids: number[]];
openClear: [formData: AlarmEventQueryRequest];
setLoading: [loading: boolean];
query: [page?: number, pageSize?: number];
}>();
const rowSelection = useRowSelection();
const userStore = useUserStore();
const queryOrder = useQueryOrder(props.tableName, DESC);
const { tableColumns, columnsHook } = useTableColumns(props.tableName, props.columns);
const { monitorMetrics } = useCacheStore();
const { getDictValue } = useDictStore();
const selectedKeys = ref<Array<number>>([]);
// 获取指标名称
const getMetricsField = (metricsId: number, field: string) => {
return (monitorMetrics as Array<MetricsQueryResponse>).find(m => m.id === metricsId)?.[field];
};
// 提取标签
const extraTags = (record: AlarmEventQueryResponse) => {
try {
const parse = JSON.parse(record.alarmTags);
const children = Object.entries(parse).map(([key, value]) => {
return h(Tag, { title: `${key}: ${value}` }, { default: () => `${key}: ${value}` });
});
return h(Space, {}, { default: () => children });
} catch (e) {
return h('span', {}, '');
}
};
// 格式化指标单位
const formatMetricsValueUnit = (value: number, record: AlarmEventQueryResponse) => {
try {
const unit = getMetricsField(record.metricsId, 'unit');
const suffix = getMetricsField(record.metricsId, 'suffix');
return MetricUnitFormatter[unit as keyof typeof MetricsUnit].format(value, { suffix });
} catch (e) {
return value;
}
};
// 标记误报
const setFalseAlarm = async (idList: Array<number>, clear: boolean) => {
Modal.confirm({
title: '误报确认',
content: `确定要标记这 ${idList.length} 条数据为误报吗?`,
onOk: async () => {
try {
emits('setLoading', true);
// 调用设置误报
await setAlarmEventFalse({ idList });
Message.success('已标记为误报');
if (clear) {
selectedKeys.value = [];
}
props.tableData.filter(s => idList.includes(s.id)).forEach(s => {
s.falseAlarm = FalseAlarm.TRUE;
});
} catch (e) {
} finally {
emits('setLoading', false);
}
}
});
};
// 删除数据
const deleteRows = async (idList: Array<number>) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除这 ${idList.length} 条数据吗?`,
onOk: async () => {
try {
emits('setLoading', true);
// 调用删除接口
await batchDeleteAlarmEvent(idList);
Message.success(`成功删除 ${idList.length} 条数据`);
selectedKeys.value = [];
// 重新加载
emits('query');
} catch (e) {
} finally {
emits('setLoading', false);
}
}
});
};
// 告警处理回调
const alarmHandled = (request: Required<AlarmEventHandleRequest>) => {
props.tableData.filter(s => (request.idList || []).includes(s.id)).forEach(s => {
s.handleTime = request.handleTime;
s.handleStatus = request.handleStatus;
s.handleRemark = request.handleRemark;
s.handleUserId = userStore.id as number;
s.handleUsername = userStore.username as string;
});
selectedKeys.value = [];
};
defineExpose({ alarmHandled });
</script>
<style lang="less" scoped>
.info-wrapper {
padding: 4px 0;
.info-item {
display: flex;
&:not(:last-child) {
margin-bottom: 4px;
}
.info-label {
width: 60px;
margin-right: 8px;
user-select: none;
font-weight: 600;
&::after {
content: ':';
}
}
.info-value {
width: calc(100% - 68px);
}
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<!-- 搜索 -->
<a-card class="general-card table-search-card">
<query-header :model="formModel"
label-align="left"
@submit="fetchTableData"
@reset="fetchTableData"
@keyup.enter="() => fetchTableData()">
<!-- 处理状态 -->
<a-form-item field="handleStatus" label="处理状态">
<a-select v-model="formModel.handleStatus"
:options="toOptions(HandleStatusKey)"
placeholder="请选择处理状态"
allow-clear />
</a-form-item>
<!-- 告警主机 -->
<a-form-item field="hostId" label="告警主机">
<host-selector v-model="formModel.hostId"
placeholder="请选择告警主机"
hide-button
allow-clear />
</a-form-item>
<!-- 告警级别 -->
<a-form-item field="alarmLevel" label="告警级别">
<a-select v-model="formModel.alarmLevel"
:options="toOptions(AlarmLevelKey)"
placeholder="请选择告警级别"
allow-clear />
</a-form-item>
<!-- 处理人 -->
<a-form-item field="handleUserId" label="处理人">
<user-selector v-model="formModel.handleUserId"
placeholder="请选择处理人"
hide-button
allow-clear />
</a-form-item>
<!-- 处理备注 -->
<a-form-item field="handleRemark" label="处理备注">
<a-input v-model="formModel.handleRemark"
placeholder="请输入处理备注"
allow-clear />
</a-form-item>
<!-- 告警策略 -->
<a-form-item field="policyId" label="告警策略">
<alarm-policy-selector v-model="formModel.policyId"
placeholder="请输入告警策略"
hide-button
allow-clear />
</a-form-item>
<!-- 数据集 -->
<a-form-item field="metricsId" label="数据集">
<a-select v-model="formModel.metricsMeasurement"
:options="toOptions(MetricsMeasurementKey)"
placeholder="数据集"
allow-clear />
</a-form-item>
<!-- 告警指标 -->
<a-form-item field="metricsId" label="告警指标">
<monitor-metrics-selector v-model="formModel.metricsId"
placeholder="请选择告警指标"
hide-button
allow-clear />
</a-form-item>
<!-- 是否误报 -->
<a-form-item field="falseAlarm" label="是否误报">
<a-select v-model="formModel.falseAlarm"
:options="toOptions(FalseAlarmKey)"
placeholder="请选择是否误报"
allow-clear />
</a-form-item>
<!-- id -->
<a-form-item field="id" label="id">
<a-input-number v-model="formModel.id"
placeholder="请输入id"
hide-button
allow-clear />
</a-form-item>
<!-- agentKey -->
<a-form-item field="agentKey" label="agentKey">
<a-input v-model="formModel.agentKey"
placeholder="请输入agentKey"
allow-clear />
</a-form-item>
<!-- 告警时间 -->
<a-form-item field="createTimeRange" label="告警时间">
<a-range-picker v-model="formModel.createTimeRange"
style="width: 100%;"
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
show-time
format="YYYY-MM-DD HH:mm:ss" />
</a-form-item>
</query-header>
</a-card>
<!-- 表格 -->
<alarm-event-table-base ref="eventTable"
:table-name="TableName"
:columns="columns"
:table-data="tableRenderData"
:loading="loading"
:form-model="formModel"
:pagination="pagination"
:show-clear-button="true"
@open-handle="emits('openHandle', $event)"
@open-clear="emits('openClear', formModel)"
@set-loading="setLoading"
@query="fetchTableData" />
</template>
<script lang="ts">
export default {
name: 'alarmEventTable'
};
</script>
<script lang="ts" setup>
import type { AlarmEventQueryRequest, AlarmEventQueryResponse, AlarmEventHandleRequest } from '@/api/monitor/alarm-event';
import { reactive, ref, onMounted } from 'vue';
import { getAlarmEventPage } from '@/api/monitor/alarm-event';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { TableName, FalseAlarm, HandleStatusKey, FalseAlarmKey, MetricsMeasurementKey, AlarmLevelKey } from '../types/const';
import { useTablePagination } from '@/hooks/table';
import { useRoute } from 'vue-router';
import { useDictStore } from '@/store';
import { useQueryOrder, DESC } from '@/hooks/query-order';
import UserSelector from '@/components/user/user/selector/index.vue';
import HostSelector from '@/components/asset/host/selector/index.vue';
import MonitorMetricsSelector from '@/components/monitor/metrics/selector/index.vue';
import AlarmPolicySelector from '@/components/monitor/alarm-policy/selector/index.vue';
import AlarmEventTableBase from './alarm-event-table-base.vue';
const emits = defineEmits(['openHandle', 'openClear']);
const eventTable = ref();
const pagination = useTablePagination();
const { toOptions } = useDictStore();
const { loading, setLoading } = useLoading();
const queryOrder = useQueryOrder(TableName, DESC);
const tableRenderData = ref<Array<AlarmEventQueryResponse>>([]);
const formModel = reactive<AlarmEventQueryRequest>({
id: undefined,
agentKey: undefined,
hostId: undefined,
policyId: undefined,
metricsId: undefined,
metricsMeasurement: undefined,
alarmLevel: undefined,
falseAlarm: FalseAlarm.FALSE,
handleStatus: undefined,
handleRemark: undefined,
handleUserId: undefined,
createTimeRange: [],
});
// 重新加载
const reload = () => {
// 重新加载数据
fetchTableData();
};
// 告警处理回调
const alarmHandled = (request: Required<AlarmEventHandleRequest>) => {
eventTable.value.alarmHandled(request);
};
defineExpose({ reload, alarmHandled });
// 加载数据
const doFetchTableData = async (request: AlarmEventQueryRequest) => {
try {
setLoading(true);
const { data } = await getAlarmEventPage(queryOrder.markOrderly(request));
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
onMounted(() => {
const key = useRoute().query.key as string;
if (key) {
formModel.id = Number.parseInt(key);
}
fetchTableData();
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,48 @@
<template>
<div class="layout-container" v-if="render">
<!-- 列表-表格 -->
<alarm-event-table ref="table"
@open-handle="(e: any) => handleModal.open(e)"
@open-clear="(e: any) => clearModal.open(e)" />
<!-- 处理模态框-->
<alarm-event-handle-modal ref="handleModal"
@handled="(e: any) => table.alarmHandled(e)" />
<!-- 清理模态框-->
<alarm-event-clear-modal ref="clearModal"
@clear="() => table.reload()" />
</div>
</template>
<script lang="ts">
export default {
name: 'alarmEvent'
};
</script>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue';
import { useDictStore, useCacheStore } from '@/store';
import { dictKeys } from './types/const';
import AlarmEventTable from './components/alarm-event-table.vue';
import AlarmEventClearModal from './components/alarm-event-clear-modal.vue';
import AlarmEventHandleModal from './components/alarm-event-handle-modal.vue';
const render = ref(false);
const table = ref();
const handleModal = ref();
const clearModal = ref();
onBeforeMount(async () => {
const cacheStore = useCacheStore();
await cacheStore.loadMonitorAlarmPolicy();
await cacheStore.loadMonitorMetricsList();
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
render.value = true;
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,30 @@
export const TableName = 'alarm_event';
// 最大清理数量
export const maxClearLimit = 2000;
// 是否为误报
export const FalseAlarm = {
// 误报
TRUE: 1,
// 非误报
FALSE: 0,
};
// 告警条件 字典项
export const TriggerConditionKey = 'alarmTriggerCondition';
// 告警记录处理状态 字典项
export const HandleStatusKey = 'alarmEventHandleStatus';
// 是否为误报 字典项
export const FalseAlarmKey = 'falseAlarm';
// 指标数据集 字典项
export const MetricsMeasurementKey = 'metricsMeasurement';
// 告警等级 字典项
export const AlarmLevelKey = 'alarmLevel';
// 加载的字典值
export const dictKeys = [TriggerConditionKey, HandleStatusKey, FalseAlarmKey, MetricsMeasurementKey, AlarmLevelKey];

View File

@@ -0,0 +1,22 @@
import type { FieldRule } from '@arco-design/web-vue';
export const handleRules = {
handleStatus: [{
required: true,
message: '请输入处理状态'
}, {
maxLength: 16,
message: '处理状态长度不能大于16位'
}],
handleTime: [{
required: true,
message: '请输入处理时间'
}],
handleRemark: [{
required: true,
message: '请输入处理备注'
}, {
maxLength: 512,
message: '处理备注长度不能大于512位'
}],
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -0,0 +1,146 @@
import type { TableColumnData } from '@arco-design/web-vue';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 98,
align: 'left',
fixed: 'left',
default: true,
}, {
title: '主机信息',
dataIndex: 'hostInfo',
slotName: 'hostInfo',
width: 248,
align: 'left',
fixed: 'left',
default: true,
}, {
title: '处理状态',
dataIndex: 'handleStatus',
slotName: 'handleStatus',
width: 108,
align: 'left',
fixed: 'left',
default: true,
}, {
title: '告警级别',
dataIndex: 'alarmLevel',
slotName: 'alarmLevel',
align: 'left',
width: 108,
default: true,
}, {
title: '告警策略',
dataIndex: 'policyId',
slotName: 'policyId',
align: 'left',
width: 120,
default: false,
}, {
title: '指标数据集',
dataIndex: 'metricsMeasurement',
slotName: 'metricsMeasurement',
align: 'left',
width: 108,
ellipsis: true,
tooltip: true,
default: false,
}, {
title: '告警指标',
dataIndex: 'metricsId',
slotName: 'metricsId',
align: 'left',
width: 184,
default: false,
}, {
title: '告警标签',
dataIndex: 'alarmTags',
slotName: 'alarmTags',
align: 'left',
minWidth: 168,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '告警值',
dataIndex: 'alarmValue',
slotName: 'alarmValue',
width: 148,
align: 'left',
default: true,
}, {
title: '告警阈值',
dataIndex: 'alarmThreshold',
slotName: 'alarmThreshold',
width: 148,
align: 'left',
default: false,
}, {
title: '告警摘要',
dataIndex: 'alarmInfo',
slotName: 'alarmInfo',
align: 'left',
width: 248,
tooltip: true,
default: true,
}, {
title: '连续触发次数',
dataIndex: 'consecutiveCount',
slotName: 'consecutiveCount',
align: 'left',
width: 116,
default: true,
}, {
title: '处理人',
dataIndex: 'handleUsername',
slotName: 'handleUsername',
align: 'left',
width: 138,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '处理备注',
dataIndex: 'handleRemark',
slotName: 'handleRemark',
align: 'left',
minWidth: 128,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '处理时间',
dataIndex: 'handleTime',
slotName: 'handleTime',
align: 'left',
width: 180,
render: ({ record }) => {
return record.handleTime && dateFormat(new Date(record.handleTime));
},
default: true,
}, {
title: '告警时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
fixed: 'right',
default: true,
}, {
title: '操作',
slotName: 'handle',
width: 130,
align: 'center',
fixed: 'right',
default: true,
},
] as TableColumnData[];
export default columns;

View File

@@ -0,0 +1,173 @@
<template>
<a-modal v-model:visible="visible"
modal-class="modal-form-large"
title-align="start"
:title="title"
:top="80"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="formRules">
<!-- 策略名称 -->
<a-form-item field="name" label="策略名称">
<a-input v-model="formModel.name"
placeholder="请输入策略名称"
allow-clear />
</a-form-item>
<!-- 策略描述 -->
<a-form-item field="description" label="策略描述">
<a-input v-model="formModel.description"
placeholder="请输入策略描述"
allow-clear />
</a-form-item>
<!-- 通知模板 -->
<a-form-item field="notifyIdList" label="通知模板">
<notify-template-selector v-model="formModel.notifyIdList"
biz-type="ALARM"
multiple
@change="setChangeNotify(true)" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'alarmPolicyFormModal'
};
</script>
<script lang="ts" setup>
import type { AlarmPolicyUpdateRequest } from '@/api/monitor/alarm-policy';
import type { FormHandle } from '@/types/form';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/form.rules';
import { assignOmitRecord } from '@/utils';
import { createAlarmPolicy, updateAlarmPolicy, copyAlarmPolicy, getAlarmPolicy } from '@/api/monitor/alarm-policy';
import { Message } from '@arco-design/web-vue';
import { useToggle } from '@vueuse/core';
import NotifyTemplateSelector from '@/components/system/notify-template/selector/index.vue';
const emits = defineEmits(['added', 'updated']);
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const [changeNotify, setChangeNotify] = useToggle<boolean>(false);
const title = ref<string>();
const formHandle = ref<FormHandle>('add');
const formRef = ref<any>();
const formModel = ref<AlarmPolicyUpdateRequest>({});
const defaultForm = (): AlarmPolicyUpdateRequest => {
return {
id: undefined,
name: undefined,
description: undefined,
notifyIdList: [],
};
};
// 打开新增
const openAdd = () => {
title.value = '添加告警策略';
formHandle.value = 'add';
formModel.value = defaultForm();
setVisible(true);
};
// 打开修改
const openUpdate = async (record: any) => {
title.value = '修改告警策略';
formHandle.value = 'update';
formModel.value = defaultForm();
setVisible(true);
await renderForm(record.id);
};
// 打开修改
const openCopy = async (record: any) => {
title.value = '复制告警策略';
formHandle.value = 'copy';
formModel.value = defaultForm();
setVisible(true);
await renderForm(record.id);
};
// 渲染表单
const renderForm = async (id: number) => {
try {
setLoading(true);
const { data } = await getAlarmPolicy(id);
formModel.value = assignOmitRecord(data);
} catch (e) {
} finally {
setLoading(false);
}
};
defineExpose({ openAdd, openUpdate, openCopy });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
if (formHandle.value === 'copy') {
// 复制
await copyAlarmPolicy(formModel.value);
Message.success('复制成功');
emits('added');
} else if (formHandle.value === 'add') {
// 新增
await createAlarmPolicy(formModel.value);
Message.success('创建成功');
emits('added');
} else {
// 修改
await updateAlarmPolicy({ ...formModel.value, updateNotify: changeNotify.value });
Message.success('修改成功');
emits('updated');
}
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
setChangeNotify(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,230 @@
<template>
<!-- 搜索 -->
<a-card class="general-card table-search-card">
<query-header :model="formModel"
label-align="left"
@submit="fetchTableData"
@reset="fetchTableData"
@keyup.enter="() => fetchTableData()">
<!-- id -->
<a-form-item field="id" label="id">
<a-input-number v-model="formModel.id"
placeholder="请输入id"
hide-button
allow-clear />
</a-form-item>
<!-- 策略名称 -->
<a-form-item field="name" label="策略名称">
<a-input v-model="formModel.name"
placeholder="请输入策略名称"
allow-clear />
</a-form-item>
<!-- 策略描述 -->
<a-form-item field="description" label="策略描述">
<a-input v-model="formModel.description"
placeholder="请输入策略描述"
allow-clear />
</a-form-item>
</query-header>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧操作 -->
<div class="table-left-bar-handle">
<!-- 标题 -->
<div class="table-title">
告警策略列表
</div>
</div>
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 新增 -->
<a-button v-permission="['monitor:alarm-policy:create']"
type="primary"
@click="emits('openAdd')">
新增
<template #icon>
<icon-plus />
</template>
</a-button>
<!-- 调整 -->
<table-adjust :columns="columns"
:columns-hook="columnsHook"
:query-order="queryOrder"
@query="fetchTableData" />
</a-space>
</div>
</template>
<!-- table -->
<a-table row-key="id"
ref="tableRef"
:loading="loading"
:columns="tableColumns"
:data="tableRenderData"
:pagination="pagination"
:bordered="false"
@page-change="(page: number) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size: number) => fetchTableData(1, size)">
<!-- 策略名称 -->
<template #name="{ record }">
<span class="ml4 span-blue pointer" @click="openRules(record)">
{{ record.name }}
</span>
</template>
<!-- 规则数量 -->
<template #ruleCount="{ record }">
<b class="ml4 span-blue pointer" @click="openRules(record)">{{ record.ruleCount }} 个</b>
</template>
<!-- 主机数量 -->
<template #hostCount="{ record }">
<b class="ml4 span-blue">{{ record.hostCount }} 个</b>
</template>
<!-- 今日触发次数 -->
<template #todayTriggerCount="{ record }">
<b class="ml4" :class="[ (record.todayTriggerCount || 0) > 0 ? 'span-red' : '' ]">{{ record.todayTriggerCount || 0 }} 次</b>
</template>
<!-- 7日触发次数 -->
<template #weekTriggerCount="{ record }">
<b class="ml4" :class="[ (record.weekTriggerCount || 0) > 0 ? 'span-red' : '' ]">{{ record.weekTriggerCount || 0 }} 次</b>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 修改 -->
<a-button v-permission="['monitor:alarm-policy:update']"
type="text"
size="mini"
@click="emits('openUpdate', record)">
修改
</a-button>
<!-- 告警规则 -->
<a-button v-permission="['monitor:alarm-policy:update']"
type="text"
size="mini"
@click="openRules(record)">
告警规则
</a-button>
<!-- 复制策略 -->
<a-button v-permission="['monitor:alarm-policy:create']"
type="text"
size="mini"
@click="emits('openCopy', record)">
复制策略
</a-button>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
position="left"
type="warning"
@ok="deleteRow(record)">
<a-button v-permission="['monitor:alarm-policy:delete']"
type="text"
size="mini"
status="danger">
删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</a-card>
</template>
<script lang="ts">
export default {
name: 'alarmPolicyTable'
};
</script>
<script lang="ts" setup>
import type { AlarmPolicyQueryRequest, AlarmPolicyQueryResponse } from '@/api/monitor/alarm-policy';
import { reactive, ref, onMounted } from 'vue';
import { deleteAlarmPolicy, getAlarmPolicyPage } from '@/api/monitor/alarm-policy';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { useRouter } from 'vue-router';
import { TableName } from '../types/const';
import { useTablePagination, useTableColumns } from '@/hooks/table';
import { useQueryOrder, ASC } from '@/hooks/query-order';
import TableAdjust from '@/components/app/table-adjust/index.vue';
const emits = defineEmits(['openAdd', 'openUpdate', 'openCopy']);
const router = useRouter();
const pagination = useTablePagination();
const { loading, setLoading } = useLoading();
const queryOrder = useQueryOrder(TableName, ASC);
const { tableColumns, columnsHook } = useTableColumns(TableName, columns);
const tableRenderData = ref<Array<AlarmPolicyQueryResponse>>([]);
const formModel = reactive<AlarmPolicyQueryRequest>({
id: undefined,
name: undefined,
description: undefined,
});
// 打开规则页面
const openRules = (record: AlarmPolicyQueryResponse) => {
router.push({
name: 'alarmRule',
query: {
id: record.id,
name: record.name,
},
});
};
// 删除当前行
const deleteRow = async (record: AlarmPolicyQueryResponse) => {
try {
setLoading(true);
// 调用删除接口
await deleteAlarmPolicy(record.id);
Message.success('删除成功');
// 重新加载
reload();
} catch (e) {
} finally {
setLoading(false);
}
};
// 重新加载
const reload = () => {
// 重新加载数据
fetchTableData();
};
defineExpose({ reload });
// 加载数据
const doFetchTableData = async (request: AlarmPolicyQueryRequest) => {
try {
setLoading(true);
const { data } = await getAlarmPolicyPage(queryOrder.markOrderly(request));
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
onMounted(() => {
fetchTableData();
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="layout-container" v-if="render">
<!-- 列表-表格 -->
<alarm-policy-table ref="table"
@open-add="() => modal.openAdd()"
@open-update="(e: any) => modal.openUpdate(e)"
@open-copy="(e: any) => modal.openCopy(e)" />
<!-- 添加修改模态框 -->
<alarm-policy-form-modal ref="modal"
@added="reload"
@updated="reload" />
</div>
</template>
<script lang="ts">
export default {
name: 'alarmPolicy'
};
</script>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue';
import AlarmPolicyTable from './components/alarm-policy-table.vue';
import AlarmPolicyFormModal from './components/alarm-policy-form-modal.vue';
const render = ref(false);
const table = ref();
const modal = ref();
// 重新加载
const reload = () => {
table.value.reload();
};
onBeforeMount(async () => {
render.value = true;
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1 @@
export const TableName = 'monitor_alarm_policy';

View File

@@ -0,0 +1,20 @@
import type { FieldRule } from '@arco-design/web-vue';
const rules = {
name: [{
required: true,
message: '请输入策略名称'
}, {
maxLength: 64,
message: '策略名称长度不能大于64位'
}],
description: [{
required: true,
message: '请输入策略描述'
}, {
maxLength: 255,
message: '策略描述长度不能大于255位'
}],
} as Record<string, FieldRule | FieldRule[]>;
export default rules;

View File

@@ -0,0 +1,99 @@
import type { TableColumnData } from '@arco-design/web-vue';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 68,
align: 'left',
fixed: 'left',
default: true,
}, {
title: '策略名称',
dataIndex: 'name',
slotName: 'name',
align: 'left',
minWidth: 218,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '规则数量',
dataIndex: 'ruleCount',
slotName: 'ruleCount',
align: 'left',
width: 128,
default: true,
}, {
title: '主机数量',
dataIndex: 'hostCount',
slotName: 'hostCount',
align: 'left',
width: 128,
default: true,
}, {
title: '今日触发次数',
dataIndex: 'todayTriggerCount',
slotName: 'todayTriggerCount',
align: 'left',
width: 128,
default: true,
}, {
title: '7日触发次数',
dataIndex: 'weekTriggerCount',
slotName: 'weekTriggerCount',
align: 'left',
width: 128,
default: true,
}, {
title: '策略描述',
dataIndex: 'description',
slotName: 'description',
align: 'left',
minWidth: 238,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '创建时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
}, {
title: '修改时间',
dataIndex: 'updateTime',
slotName: 'updateTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.updateTime));
},
default: true,
}, {
title: '创建人',
width: 148,
dataIndex: 'creator',
slotName: 'creator',
}, {
title: '修改人',
width: 148,
dataIndex: 'updater',
slotName: 'updater',
default: true,
}, {
title: '操作',
slotName: 'handle',
width: 248,
align: 'center',
fixed: 'right',
default: true,
},
] as TableColumnData[];
export default columns;

View File

@@ -0,0 +1,368 @@
<template>
<a-drawer v-model:visible="visible"
:title="title"
:width="590"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@cancel="handleClose">
<a-spin class="full drawer-form-large" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="formRules">
<!-- 监控指标 -->
<a-form-item field="metricsId" label="监控指标">
<monitor-metrics-selector v-model="formModel.metricsId"
class="metrics-selector"
placeholder="请选择监控指标"
allow-clear />
<!-- 添加标签 -->
<a-button title="添加标签"
:disabled="formModel.allEffect === 1"
@click="addTag">
<template #icon>
<icon-tags />
</template>
</a-button>
</a-form-item>
<!-- tags -->
<template v-for="(tag, index) in tags">
<a-form-item v-if="formModel.allEffect === 0"
:field="'tag-' + (index + 1)"
:label="'指标标签-' + (index + 1)">
<a-space :size="12">
<!-- 标签名称 -->
<a-input v-model="tag.key"
style="width: 128px;"
placeholder="指标标签名称" />
<!-- 标签值 -->
<a-select v-model="tag.value"
class="tag-values"
style="width: 260px"
:max-tag-count="2"
placeholder="标签值"
tag-nowrap
multiple
allow-create />
<!-- 移除 -->
<a-button title="移除"
style="width: 32px"
@click="removeTag(index)">
<template #icon>
<icon-minus />
</template>
</a-button>
</a-space>
</a-form-item>
</template>
<a-row>
<!-- 规则开关 -->
<a-col :span="12" style="padding-right: 24px;">
<a-form-item field="ruleSwitch"
label="规则开关"
hide-asterisk>
<a-switch v-model="formModel.ruleSwitch"
type="round"
:checked-value="1"
:unchecked-value="0" />
</a-form-item>
</a-col>
<!-- 全部生效 -->
<a-col :span="12">
<a-form-item field="allEffect"
label="全部生效"
tooltip="开启后则忽略标签, 并生效与已配置标签的规则 (通常用于默认策略)"
hide-asterisk>
<a-switch v-model="formModel.allEffect"
type="round"
:checked-value="1"
:unchecked-value="0" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<!-- 持续数据点 -->
<a-col :span="12" style="padding-right: 24px;">
<a-form-item field="silencePeriod"
label="持续数据点"
hide-asterisk>
<a-input-number v-model="formModel.consecutiveCount"
:min="0"
:max="100"
placeholder="持续数据点"
hide-button
allow-clear>
<template #append>
<span></span>
</template>
</a-input-number>
</a-form-item>
</a-col>
<!-- 静默时间 -->
<a-col :span="12">
<a-form-item field="silencePeriod"
label="静默时间"
tooltip="再次发生告警后沉默的时间"
hide-asterisk>
<a-input-number v-model="formModel.silencePeriod"
:min="0"
placeholder="请输入静默时间"
hide-button
allow-clear>
<template #append>
<span>分钟</span>
</template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<!-- 告警条件 -->
<a-row>
<a-col :span="7">
<a-form-item field="level"
label="告警条件"
class="alarm-level-select">
<a-select v-model="formModel.level"
style="padding: 0;"
:options="toOptions(LevelKey)"
:bordered="false"
placeholder="级别">
<template #label="{ data: { label, value } }">
<a-tag :color="getDictValue(LevelKey, value,'color')">{{ label }}</a-tag>
</template>
<template #option="{ data: { label, value } }">
<a-tag style="padding: 0 3px;" :color="getDictValue(LevelKey, value,'color')">{{ label }}</a-tag>
</template>
</a-select>
</a-form-item>
</a-col>
<!-- 告警条件 -->
<a-col :span="6">
<a-form-item field="triggerCondition"
class="condition-select"
hide-label>
<a-select v-model="formModel.triggerCondition"
:options="toOptions(TriggerConditionKey)"
placeholder="请选择告警条件" />
</a-form-item>
</a-col>
<!-- 触发阈值 -->
<a-col :span="11">
<a-form-item field="threshold"
style="padding-left: 16px;"
hide-label>
<a-input-number v-model="formModel.threshold"
:precision="4"
placeholder="触发阈值"
hide-button
allow-clear>
<template v-if="metricsUnit" #append>
{{ metricsUnit }}
</template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<!-- 规则描述 -->
<a-form-item field="description" label="规则描述">
<a-textarea v-model="formModel.description"
:auto-size="{ minRows: 4, maxRows: 4 }"
placeholder="请输入规则描述"
allow-clear />
</a-form-item>
</a-form>
</a-spin>
</a-drawer>
</template>
<script lang="ts">
export default {
name: 'alarmRuleFormDrawer'
};
</script>
<script lang="ts" setup>
import type { AlarmRuleUpdateRequest } from '@/api/monitor/alarm-rule';
import type { MetricsQueryResponse } from '@/api/monitor/metrics';
import type { RuleTag } from '../types/const';
import type { FormHandle } from '@/types/form';
import { ref, computed } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/form.rules';
import { MetricsUnitKey } from '../types/const';
import { assignOmitRecord } from '@/utils';
import { TriggerConditionKey, LevelKey, DefaultCondition, DefaultLevel, } from '../types/const';
import { createAlarmRule, updateAlarmRule } from '@/api/monitor/alarm-rule';
import { Message } from '@arco-design/web-vue';
import { useDictStore, useCacheStore } from '@/store';
import MonitorMetricsSelector from '@/components/monitor/metrics/selector/index.vue';
const emits = defineEmits(['added', 'updated']);
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const { monitorMetrics } = useCacheStore();
const { getDictValue, toOptions } = useDictStore();
const title = ref<string>();
const formHandle = ref<FormHandle>('add');
const formRef = ref<any>();
const formModel = ref<AlarmRuleUpdateRequest>({});
const tags = ref<Array<RuleTag>>([]);
const defaultForm = (): AlarmRuleUpdateRequest => {
return {
id: undefined,
policyId: undefined,
metricsId: undefined,
tags: undefined,
level: DefaultLevel,
ruleSwitch: 1,
allEffect: 0,
triggerCondition: DefaultCondition,
threshold: undefined,
consecutiveCount: 1,
silencePeriod: 0,
description: undefined,
};
};
// 指标单位
const metricsUnit = computed(() => {
const metricsId = formModel.value.metricsId;
if (!metricsId) {
return '';
}
// 读取指标单位
const unit = (monitorMetrics as Array<MetricsQueryResponse>).find(m => m.id === metricsId)?.unit;
if (!unit) {
return '';
}
return getDictValue(MetricsUnitKey, unit, 'alarmUnit');
});
// 打开新增
const openAdd = (policyId: number) => {
title.value = '添加监控告警规则';
formHandle.value = 'add';
renderForm({ ...defaultForm(), policyId });
setVisible(true);
};
// 打开复制
const openCopy = (record: any) => {
title.value = '添加监控告警规则';
formHandle.value = 'add';
renderForm({ ...defaultForm(), ...record, id: undefined });
setVisible(true);
};
// 打开修改
const openUpdate = (record: any) => {
title.value = '修改监控告警规则';
formHandle.value = 'update';
renderForm({ ...defaultForm(), ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: any) => {
formModel.value = assignOmitRecord({ ...defaultForm(), ...record }, 'tags');
if (record.tags) {
tags.value = JSON.parse(record.tags);
} else {
tags.value = [];
}
};
defineExpose({ openAdd, openCopy, openUpdate });
// 添加标签
const addTag = () => {
tags.value.push({ key: '', value: [] });
};
// 移除标签
const removeTag = (index: number) => {
tags.value.splice(index, 1);
};
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
for (let tag of tags.value) {
if (!tag.key) {
Message.error('请输入标签名称');
return false;
}
if (!tag.value) {
Message.error('请输入标签值');
return false;
}
}
if (formHandle.value == 'add') {
// 新增
await createAlarmRule({
...formModel.value,
tags: formModel.value.allEffect === 1 ? '[]' : JSON.stringify(tags.value)
});
Message.success('创建成功');
emits('added');
} else {
// 修改
await updateAlarmRule({
...formModel.value,
tags: formModel.value.allEffect === 1 ? '[]' : JSON.stringify(tags.value)
});
Message.success('修改成功');
emits('updated');
}
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
:deep(.metrics-selector) {
width: calc(100% - 42px);
margin-right: 12px;
}
.alarm-level-select, .condition-select {
:deep(.arco-select-view-suffix) {
display: none !important;
}
}
:deep(.tag-values .arco-select-view-inner) {
flex-wrap: nowrap !important;
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<!-- 内容部分 -->
<div class="container-content">
<!-- 指标类型 -->
<a-card class="general-card table-search-card measurement-card">
<a-tabs v-model:active-key="measurement"
direction="vertical"
type="rounded"
:hide-content="true"
@change="reload">
<a-tab-pane key="" title="全部" />
<a-tab-pane v-for="item in toOptions(MeasurementKey)"
:key="item.value as string"
:title="item.label" />
</a-tabs>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧操作 -->
<div class="table-left-bar-handle">
<!-- 标题 -->
<div class="table-title">
告警规则 - {{ policyName }}
</div>
</div>
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 新增 -->
<a-button v-permission="['monitor:alarm-policy:update-rule']"
type="primary"
@click="emits('openAdd', policyId)">
新增
<template #icon>
<icon-plus />
</template>
</a-button>
<!-- 刷新 -->
<a-button @click="doFetchTableData">
<template #icon>
<icon-refresh />
</template>
</a-button>
<!-- 调整 -->
<table-adjust :columns="columns"
:columns-hook="columnsHook"
@query="doFetchTableData" />
</a-space>
</div>
</template>
<!-- table -->
<a-table row-key="id"
ref="tableRef"
:loading="loading"
:columns="tableColumns"
:data="tableRenderData"
:pagination="false"
:bordered="false">
<!-- 指标标签 -->
<template #tags="{ record }">
<a-tag v-if="record.allEffect === 1">
全部
</a-tag>
<a-space v-else-if="record.allEffect === 0">
<a-tag v-for="tag in extraTags(record.tags)"
class="text-ellipsis"
style="display: inline-block; max-width: 100px;">
{{ tag }}
</a-tag>
</a-space>
</template>
<!-- 告警级别 -->
<template #level="{ record }">
<a-tag :color="getDictValue(LevelKey, record.level, 'color')">
{{ getDictValue(LevelKey, record.level) }}
</a-tag>
</template>
<!-- 告警条件 -->
<template #triggerCondition="{ record }">
<span>
<!-- 指标名称 -->
<span class="mr4">{{ getMetricsField(record.metricsId, 'name') }}</span>
<!-- 条件 -->
<span class="mr4">{{ getDictValue(TriggerConditionKey, record.triggerCondition) }}</span>
<!-- 阈值 -->
<b>{{ record.threshold }}{{ getDictValue(MetricsUnitKey, getMetricsField(record.metricsId, 'unit'), 'alarmUnit') }}</b>
</span>
</template>
<!-- 静默时间 -->
<template #silencePeriod="{ record }">
{{ record.silencePeriod }} 分钟
</template>
<!-- 持续数据点 -->
<template #consecutiveCount="{ record }">
{{ record.consecutiveCount }}
</template>
<!-- 规则开关 -->
<template #ruleSwitch="{ record }">
<a-switch v-model="record.ruleSwitch"
type="round"
:disabled="!hasPermission('monitor:alarm-policy:update-rule')"
:checked-value="1"
:unchecked-value="0"
@change="(s: any) => handleSwitchChange(record, s)" />
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 修改 -->
<a-button v-permission="['monitor:alarm-policy:update-rule']"
type="text"
size="mini"
@click="emits('openUpdate', record)">
修改
</a-button>
<!-- 复制 -->
<a-button v-permission="['monitor:alarm-policy:update-rule']"
type="text"
size="mini"
@click="emits('openCopy', record)">
复制
</a-button>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
position="left"
type="warning"
@ok="deleteRow(record)">
<a-button v-permission="['monitor:alarm-policy:update-rule']"
type="text"
size="mini"
status="danger">
删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</a-card>
</div>
</template>
<script lang="ts">
export default {
name: 'alarmRuleTable'
};
</script>
<script lang="ts" setup>
import type { AlarmRuleQueryResponse } from '@/api/monitor/alarm-rule';
import type { MetricsQueryResponse } from '@/api/monitor/metrics';
import { ref, onMounted } from 'vue';
import { deleteAlarmRule, getAlarmRuleList, updateAlarmRuleSwitch } from '@/api/monitor/alarm-rule';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import usePermission from '@/hooks/permission';
import columns from '../types/table.columns';
import { useRoute } from 'vue-router';
import { useDictStore, useCacheStore } from '@/store';
import { useTableColumns } from '@/hooks/table';
import { TriggerConditionKey, LevelKey, TableName, MeasurementKey, MetricsUnitKey } from '../types/const';
import TableAdjust from '@/components/app/table-adjust/index.vue';
const emits = defineEmits(['openAdd', 'openUpdate', 'openCopy']);
const { loading, setLoading } = useLoading();
const { hasPermission } = usePermission();
const { monitorMetrics } = useCacheStore();
const { toOptions, getDictValue } = useDictStore();
const { tableColumns, columnsHook } = useTableColumns(TableName, columns);
const policyId = ref<number>(0);
const policyName = ref<string>('');
const measurement = ref<string>('');
const tableRenderData = ref<Array<AlarmRuleQueryResponse>>([]);
// 删除当前行
const deleteRow = async (record: AlarmRuleQueryResponse) => {
try {
setLoading(true);
// 调用删除接口
await deleteAlarmRule(record.id);
Message.success('删除成功');
// 重新加载
reload();
} catch (e) {
} finally {
setLoading(false);
}
};
// 提取标签
const extraTags = (tags: string) => {
if (!tags) {
return [];
}
try {
return JSON.parse(tags).map((s: any) => `${s.key}: ${s.value}`);
} catch (e) {
return [];
}
};
// 获取指标名称
const getMetricsField = (metricsId: number, field: string) => {
return (monitorMetrics as Array<MetricsQueryResponse>).find(m => m.id === metricsId)?.[field];
};
// 切换规则开关
const handleSwitchChange = async (record: AlarmRuleQueryResponse, checked: number) => {
try {
setLoading(true);
await updateAlarmRuleSwitch({
id: record.id,
ruleSwitch: checked
});
record.ruleSwitch = checked;
} catch (e) {
} finally {
setLoading(false);
}
};
// 重新加载
const reload = () => {
// 重新加载数据
doFetchTableData();
};
defineExpose({ reload });
// 加载数据
const doFetchTableData = async () => {
try {
setLoading(true);
const { data } = await getAlarmRuleList(policyId.value, measurement.value);
tableRenderData.value = data;
} catch (e) {
} finally {
setLoading(false);
}
};
onMounted(() => {
try {
// 解析参数
const route = useRoute();
policyId.value = Number.parseInt(route.query.id as string);
policyName.value = route.query.name as string;
// 重新加载数据
reload();
} catch (e) {
}
});
</script>
<style lang="less" scoped>
@measurement-card-width: 120px;
.container-content {
display: flex;
}
.measurement-card {
width: @measurement-card-width;
margin: 0 16px 0 0 !important;
user-select: none;
}
.table-card {
width: calc(100% - @measurement-card-width - 16px);
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<div class="layout-container" v-if="render">
<!-- 列表-表格 -->
<alarm-rule-table ref="table"
@open-add="(e: any) => drawer.openAdd(e)"
@open-copy="(e: any) => drawer.openCopy(e)"
@open-update="(e: any) => drawer.openUpdate(e)" />
<!-- 添加修改抽屉 -->
<alarm-rule-form-drawer ref="drawer"
@added="reload"
@updated="reload" />
</div>
</template>
<script lang="ts">
export default {
name: 'alarmRule'
};
</script>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue';
import { useDictStore, useCacheStore } from '@/store';
import { dictKeys } from './types/const';
import AlarmRuleTable from './components/alarm-rule-table.vue';
import AlarmRuleFormDrawer from './components/alarm-rule-form-drawer.vue';
const render = ref(false);
const table = ref();
const drawer = ref();
// 重新加载
const reload = () => {
table.value.reload();
};
onBeforeMount(async () => {
await useCacheStore().loadMonitorMetricsList();
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
render.value = true;
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,32 @@
// 告警规则标签
export interface RuleTag {
key: string;
value: string[];
}
// 表格名称
export const TableName = 'alarm-rule';
// 默认告警条件
export const DefaultCondition = 'GE';
// 默认告警等级
export const DefaultLevel = 0;
// 指标度量 字典项
export const MeasurementKey = 'metricsMeasurement';
// 监控指标单位 字典项
export const MetricsUnitKey = 'metricsUnit';
// 规则开关 字典项
export const RuleSwitchKey = 'monitorAlarmSwitch';
// 告警条件 字典项
export const TriggerConditionKey = 'alarmTriggerCondition';
// 告警等级 字典项
export const LevelKey = 'alarmLevel';
// 加载的字典值
export const dictKeys = [MetricsUnitKey, MeasurementKey, TriggerConditionKey, RuleSwitchKey, LevelKey];

View File

@@ -0,0 +1,45 @@
import type { FieldRule } from '@arco-design/web-vue';
const rules = {
policyId: [{
required: true,
message: '请输入策略id'
}],
metricsId: [{
required: true,
message: '请输入指标id'
}],
ruleSwitch: [{
required: true,
message: '请输入规则开关'
}],
level: [{
required: true,
message: '请输入告警级别'
}],
triggerCondition: [{
required: true,
message: '请输入告警条件'
}, {
maxLength: 8,
message: '告警条件长度不能大于8位'
}],
threshold: [{
required: true,
message: '请输入触发阈值'
}],
silencePeriod: [{
required: true,
message: '请输入静默时间'
}],
consecutiveCount: [{
required: true,
message: '请输入持续数据点'
}],
description: [{
maxLength: 255,
message: '规则描述长度不能大于255位'
}],
} as Record<string, FieldRule | FieldRule[]>;
export default rules;

View File

@@ -0,0 +1,107 @@
import type { TableColumnData } from '@arco-design/web-vue';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 68,
align: 'left',
fixed: 'left',
default: true,
}, {
title: '告警条件',
dataIndex: 'triggerCondition',
slotName: 'triggerCondition',
align: 'left',
minWidth: 348,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '指标标签',
dataIndex: 'tags',
slotName: 'tags',
align: 'left',
width: 168,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '告警级别',
dataIndex: 'level',
slotName: 'level',
align: 'left',
width: 120,
default: true,
}, {
title: '持续数据点',
dataIndex: 'consecutiveCount',
slotName: 'consecutiveCount',
align: 'left',
width: 108,
default: true,
}, {
title: '静默时间',
dataIndex: 'silencePeriod',
slotName: 'silencePeriod',
align: 'left',
width: 108,
default: true,
}, {
title: '规则开关',
dataIndex: 'ruleSwitch',
slotName: 'ruleSwitch',
align: 'left',
width: 118,
default: true,
}, {
title: '规则描述',
dataIndex: 'description',
slotName: 'description',
align: 'left',
minWidth: 128,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '创建时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
}, {
title: '修改时间',
dataIndex: 'updateTime',
slotName: 'updateTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.updateTime));
},
default: true,
}, {
title: '创建人',
width: 148,
dataIndex: 'creator',
slotName: 'creator',
}, {
title: '修改人',
width: 148,
dataIndex: 'updater',
slotName: 'updater',
}, {
title: '操作',
slotName: 'handle',
width: 168,
align: 'center',
fixed: 'right',
default: true,
},
] as TableColumnData[];
export default columns;

View File

@@ -28,6 +28,7 @@
<a-form-item field="measurement" label="数据集">
<a-select v-model="formModel.measurement"
:options="toOptions(MeasurementKey)"
:disabled="formHandle === 'update'"
placeholder="请选择数据集"
allow-clear />
</a-form-item>
@@ -71,6 +72,7 @@
</script>
<script lang="ts" setup>
import type { FormHandle } from '@/types/form';
import type { MetricsUpdateRequest } from '@/api/monitor/metrics';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
@@ -81,6 +83,7 @@
import { Message } from '@arco-design/web-vue';
import { useDictStore } from '@/store';
import { MetricsUnit } from '@/utils/metrics';
import { assignOmitRecord } from '@/utils';
const emits = defineEmits(['added', 'updated']);
@@ -89,7 +92,7 @@
const { toOptions } = useDictStore();
const title = ref<string>();
const isAddHandle = ref<boolean>(true);
const formHandle = ref<FormHandle>('add');
const formRef = ref<any>();
const formModel = ref<MetricsUpdateRequest>({});
@@ -108,24 +111,19 @@
// 打开新增
const openAdd = () => {
title.value = '添加监控指标';
isAddHandle.value = true;
renderForm({ ...defaultForm() });
formHandle.value = 'add';
formModel.value = assignOmitRecord({ ...defaultForm() });
setVisible(true);
};
// 打开修改
const openUpdate = (record: any) => {
title.value = '修改监控指标';
isAddHandle.value = false;
renderForm({ ...defaultForm(), ...record });
formHandle.value = 'update';
formModel.value = assignOmitRecord({ ...defaultForm(), ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: any) => {
formModel.value = Object.assign({}, record);
};
defineExpose({ openAdd, openUpdate });
// 确定
@@ -141,7 +139,7 @@
if (MetricsUnit.TEXT !== formModel.value.unit) {
formModel.value.suffix = '';
}
if (isAddHandle.value) {
if (formHandle.value === 'add') {
// 新增
await createMetrics(formModel.value);
Message.success('创建成功');

View File

@@ -3,7 +3,7 @@
<!-- 列表-表格 -->
<metrics-table ref="table"
@open-add="() => modal.openAdd()"
@open-update="(e) => modal.openUpdate(e)" />
@open-update="(e: any) => modal.openUpdate(e)" />
<!-- 添加修改模态框 -->
<metrics-form-modal ref="modal"
@added="reload"

View File

@@ -1,52 +1,42 @@
import type { FieldRule } from '@arco-design/web-vue';
export const name = [{
required: true,
message: '请输入指标名称'
}, {
maxLength: 64,
message: '指标名称长度不能大于64位'
}] as FieldRule[];
export const measurement = [{
required: true,
message: '请输入数据集'
}, {
maxLength: 64,
message: '数据集长度不能大于64位'
}] as FieldRule[];
export const value = [{
required: true,
message: '请输入指标项'
}, {
maxLength: 128,
message: '指标项长度不能大于128位'
}] as FieldRule[];
export const unit = [{
required: true,
message: '请选择单位'
}] as FieldRule[];
export const suffix = [{
required: true,
message: '请输入后缀文本'
}, {
maxLength: 32,
message: '后缀文本长度不能大于32位'
}] as FieldRule[];
export const description = [{
maxLength: 128,
message: '指标描述长度不能大于128位'
}] as FieldRule[];
export default {
name,
measurement,
value,
unit,
suffix,
description,
const rules = {
name: [{
required: true,
message: '请输入指标名称'
}, {
maxLength: 64,
message: '指标名称长度不能大于64位'
}],
measurement: [{
required: true,
message: '请输入数据集'
}, {
maxLength: 64,
message: '数据集长度不能大于64位'
}],
value: [{
required: true,
message: '请输入指标项'
}, {
maxLength: 128,
message: '指标项长度不能大于128位'
}],
unit: [{
required: true,
message: '请选择单位'
}],
suffix: [{
required: true,
message: '请输入后缀文本'
}, {
maxLength: 32,
message: '后缀文本长度不能大于32位'
}],
description: [{
maxLength: 128,
message: '指标描述长度不能大于128位'
}],
} as Record<string, FieldRule | FieldRule[]>;
export default rules;

View File

@@ -0,0 +1,186 @@
<template>
<!-- 搜索 -->
<a-card class="general-card table-search-card">
<query-header :model="formModel"
label-align="left"
@submit="fetchTableData"
@reset="fetchTableData"
@keyup.enter="() => fetchTableData()">
<!-- 处理状态 -->
<a-form-item field="handleStatus" label="处理状态">
<a-select v-model="formModel.handleStatus"
:options="toOptions(HandleStatusKey)"
placeholder="请选择处理状态"
allow-clear />
</a-form-item>
<!-- 告警级别 -->
<a-form-item field="alarmLevel" label="告警级别">
<a-select v-model="formModel.alarmLevel"
:options="toOptions(AlarmLevelKey)"
placeholder="请选择告警级别"
allow-clear />
</a-form-item>
<!-- 处理人 -->
<a-form-item field="handleUserId" label="处理人">
<user-selector v-model="formModel.handleUserId"
placeholder="请选择处理人"
hide-button
allow-clear />
</a-form-item>
<!-- 处理备注 -->
<a-form-item field="handleRemark" label="处理备注">
<a-input v-model="formModel.handleRemark"
placeholder="请输入处理备注"
allow-clear />
</a-form-item>
<!-- 告警策略 -->
<a-form-item field="policyId" label="告警策略">
<alarm-policy-selector v-model="formModel.policyId"
placeholder="请输入告警策略"
hide-button
allow-clear />
</a-form-item>
<!-- 数据集 -->
<a-form-item field="metricsId" label="数据集">
<a-select v-model="formModel.metricsMeasurement"
:options="toOptions(MetricsMeasurementKey)"
placeholder="数据集"
allow-clear />
</a-form-item>
<!-- 告警指标 -->
<a-form-item field="metricsId" label="告警指标">
<monitor-metrics-selector v-model="formModel.metricsId"
placeholder="请选择告警指标"
hide-button
allow-clear />
</a-form-item>
<!-- 是否误报 -->
<a-form-item field="falseAlarm" label="是否误报">
<a-select v-model="formModel.falseAlarm"
:options="toOptions(FalseAlarmKey)"
placeholder="请选择是否误报"
allow-clear />
</a-form-item>
<!-- id -->
<a-form-item field="id" label="id">
<a-input-number v-model="formModel.id"
placeholder="请输入id"
hide-button
allow-clear />
</a-form-item>
<!-- 告警时间 -->
<a-form-item field="createTimeRange" label="告警时间">
<a-range-picker v-model="formModel.createTimeRange"
style="width: 100%;"
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
show-time
format="YYYY-MM-DD HH:mm:ss" />
</a-form-item>
</query-header>
</a-card>
<!-- 表格 -->
<alarm-event-table-base ref="eventTable"
:table-name="TableName"
:columns="originColumns"
:table-data="tableRenderData"
:loading="loading"
:form-model="formModel"
:pagination="pagination"
:show-clear-button="false"
@open-handle="handleModal.open($event)"
@set-loading="setLoading"
@query="fetchTableData" />
<!-- 处理模态框-->
<alarm-event-handle-modal ref="handleModal"
@handled="(e: any) => eventTable.alarmHandled(e)" />
</template>
<script lang="ts">
export default {
name: 'alarmEventTab'
};
</script>
<script lang="ts" setup>
import type { AlarmEventQueryRequest, AlarmEventQueryResponse, AlarmEventHandleRequest } from '@/api/monitor/alarm-event';
import { reactive, ref, onMounted } from 'vue';
import { getAlarmEventPage } from '@/api/monitor/alarm-event';
import useLoading from '@/hooks/loading';
import columns from '../../alarm-event/types/table.columns';
import { FalseAlarm, HandleStatusKey, FalseAlarmKey, MetricsMeasurementKey, AlarmLevelKey } from '../../alarm-event/types/const';
import { TableName } from '../types/const';
import { useTablePagination } from '@/hooks/table';
import { useDictStore } from '@/store';
import { useQueryOrder, DESC } from '@/hooks/query-order';
import UserSelector from '@/components/user/user/selector/index.vue';
import MonitorMetricsSelector from '@/components/monitor/metrics/selector/index.vue';
import AlarmPolicySelector from '@/components/monitor/alarm-policy/selector/index.vue';
import AlarmEventTableBase from '@/views/monitor/alarm-event/components/alarm-event-table-base.vue';
import AlarmEventHandleModal from '@/views/monitor/alarm-event/components/alarm-event-handle-modal.vue';
const props = defineProps<{
agentKey: string;
}>();
const eventTable = ref();
const handleModal = ref();
const pagination = useTablePagination();
const queryOrder = useQueryOrder(TableName, DESC);
const { loading, setLoading } = useLoading();
const { toOptions } = useDictStore();
const originColumns = columns.filter(s => s.dataIndex !== 'hostInfo');
const tableRenderData = ref<Array<AlarmEventQueryResponse>>([]);
const formModel = reactive<AlarmEventQueryRequest>({
id: undefined,
agentKey: undefined,
policyId: undefined,
metricsId: undefined,
metricsMeasurement: undefined,
alarmLevel: undefined,
falseAlarm: FalseAlarm.FALSE,
handleStatus: undefined,
handleRemark: undefined,
handleUserId: undefined,
createTimeRange: [],
});
// 重新加载
const reload = () => {
// 重新加载数据
fetchTableData();
};
// 告警处理回调
const alarmHandled = (request: Required<AlarmEventHandleRequest>) => {
eventTable.value.alarmHandled(request);
};
defineExpose({ reload, alarmHandled });
// 加载数据
const doFetchTableData = async (request: AlarmEventQueryRequest) => {
try {
setLoading(true);
const { data } = await getAlarmEventPage(queryOrder.markOrderly({ ...request, agentKey: props.agentKey }));
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
onMounted(reload);
</script>
<style lang="less" scoped>
</style>

View File

@@ -7,7 +7,9 @@
<a-tabs v-model:active-key="activeKey"
type="rounded"
:hide-content="true">
<a-tab-pane :key="TabKeys.OVERVIEW" title="主机概览" />
<a-tab-pane :key="TabKeys.CHART" title="监控图表" />
<a-tab-pane :key="TabKeys.ALARM" title="告警记录" />
</a-tabs>
<a-divider direction="vertical"
style="height: 22px; margin: 0 16px 0 8px;"
@@ -40,8 +42,12 @@
</div>
<!-- 右侧 -->
<div class="header-right">
<!-- 告警记录标签 -->
<div v-if="activeKey === TabKeys.OVERVIEW" class="handle-wrapper">
<a-tag v-if="overrideTimestamp">更新时间: {{ dateFormat(new Date(overrideTimestamp)) }}</a-tag>
</div>
<!-- 监控图表操作 -->
<div v-if="activeKey === TabKeys.CHART" class="chart-handle">
<div v-else-if="activeKey === TabKeys.CHART" class="handle-wrapper">
<a-space>
<!-- 表格时间区间 -->
<a-select v-model="chartRange"
@@ -95,12 +101,14 @@
import { ref, onMounted, nextTick } from 'vue';
import { copy } from '@/hooks/copy';
import { useDictStore } from '@/store';
import { dateFormat } from '@/utils';
import { TabKeys, ChartRangeKey } from '../types/const';
import { OnlineStatusKey } from '@/views/monitor/monitor-host/types/const';
import { parseWindowUnit, WindowUnitFormatter } from '@/utils/metrics';
defineProps<{
host: HostQueryResponse;
overrideTimestamp: number;
}>();
const emits = defineEmits(['reloadChart']);
const activeKey = defineModel('activeKey', { type: String });
@@ -199,7 +207,7 @@
.header-right {
padding-right: 16px;
.chart-handle {
.handle-wrapper {
display: flex;
}
}

View File

@@ -0,0 +1,522 @@
<template>
<div class="host-overview">
<a-row :gutter="[24, 24]" align="stretch">
<!-- 主机信息 -->
<a-col :span="8">
<a-card v-if="host && host.spec"
class="host-info-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ padding: '0', height: '328px' }">
<template #title>
<h3>主机信息</h3>
</template>
<div class="host-info-content">
<a-descriptions :column="1"
:label-style="{ width: '100px' }"
:value-style="{ fontWeight: '600' }">
<a-descriptions-item label="SN">{{ host.spec?.sn || '-' }}</a-descriptions-item>
<a-descriptions-item label="系统名称">{{ host.spec?.osName || '-' }}</a-descriptions-item>
<a-descriptions-item label="系统类型">{{ host.osType }} - {{ host.archType }}</a-descriptions-item>
<a-descriptions-item label="CPU型号">{{ host.spec?.cpuModel || '-' }}</a-descriptions-item>
<a-descriptions-item label="CPU核心">
{{ host.spec?.cpuPhysicalCore ? `${host.spec.cpuPhysicalCore}` : '-' }}
{{ host.spec?.cpuLogicalCore ? `${host.spec.cpuLogicalCore} 线程` : '-' }}
</a-descriptions-item>
<a-descriptions-item label="CPU频率">{{ host.spec?.cpuFrequency ? `${host.spec.cpuFrequency} GHz` : '-' }}</a-descriptions-item>
<a-descriptions-item label="内存大小">{{ host.spec?.memorySize ? `${host.spec.memorySize} GB` : '-' }}</a-descriptions-item>
<a-descriptions-item label="磁盘大小">{{ host.spec?.diskSize ? `${host.spec.diskSize} GB` : '-' }}</a-descriptions-item>
<a-descriptions-item label="网络带宽">
{{ host.spec?.inBandwidth ? `${host.spec.inBandwidth} Mbps` : '-' }}
/ {{ host.spec?.outBandwidth ? `${host.spec.outBandwidth} Mbps` : '-' }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-card>
</a-col>
<!-- 第一层加载中 -->
<a-col v-if="renderLoading" :span="16">
<a-card class="metric-card"
size="small"
:bordered="false"
style="height: 376px;"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ padding: '24px' }">
<a-skeleton :animation="true">
<a-skeleton-line :rows="5"
:line-height="56"
:line-spacing="12" />
</a-skeleton>
</a-card>
</a-col>
<!-- 第一层无数据 -->
<a-col v-else-if="nodata" :span="16">
<a-card class="metric-card"
size="small"
:bordered="false"
style="height: 376px;"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ padding: '24px' }">
<a-empty style="margin-top: 88px;" />
</a-card>
</a-col>
<!-- 第一层数据 -->
<a-col v-else :span="16">
<a-row :gutter="[24, 24]">
<!-- cpu -->
<a-col :span="12">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<h3>CPU</h3>
</template>
<div class="card-content">
<a-statistic title="总计"
:value="cpuMetrics.total"
:precision="2"
:value-style="{ color: getPercentProgressColor(cpuMetrics.total / 100, '') }">
<template #suffix>%</template>
</a-statistic>
<a-statistic title="用户态"
:value="cpuMetrics.user"
:precision="2"
:value-style="{ color: getPercentProgressColor(cpuMetrics.user / 100, '') }">
<template #suffix>%</template>
</a-statistic>
<a-statistic title="内核态"
:value="cpuMetrics.system"
:precision="2"
:value-style="{ color: getPercentProgressColor(cpuMetrics.system / 100, '') }">
<template #suffix>%</template>
</a-statistic>
</div>
</a-card>
</a-col>
<!-- 内存 -->
<a-col :span="12">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<h3>内存</h3>
<a-space>
<a-tag color="arcoblue">total: {{ memoryMetrics.total }}</a-tag>
<a-tag color="purple">swap: {{ memoryMetrics.swapTotal }}</a-tag>
</a-space>
</template>
<div class="card-content">
<a-statistic title="已使用"
:value="memoryMetrics.used"
:precision="2"
:value-style="{ color: getPercentProgressColor(memoryMetrics.usedPercent / 100, '') }">
<template #suffix>{{ memoryMetrics.usedUnit }}<span style="margin: 0 4px;" />{{ memoryMetrics.usedPercent.toFixed(2) }}%</template>
</a-statistic>
<a-statistic title="交换分区"
:value="memoryMetrics.swapUsed"
:precision="2"
:value-style="{ color: getPercentProgressColor(memoryMetrics.swapUsedPercent, '') }">
<template #suffix>{{ memoryMetrics.swapUsedUnit }}<span style="margin: 0 4px;" />{{ memoryMetrics.swapUsedPercent.toFixed(2) }}%</template>
</a-statistic>
</div>
</a-card>
</a-col>
<!-- 负载 -->
<a-col :span="12">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<h3>负载</h3>
</template>
<div class="card-content">
<a-statistic title="1分钟"
:value="loadMetrics.load1"
:precision="2">
</a-statistic>
<a-statistic title="5分钟"
:value="loadMetrics.load5"
:precision="2">
</a-statistic>
<a-statistic title="15分钟"
:value="loadMetrics.load15"
:precision="2">
</a-statistic>
</div>
</a-card>
</a-col>
<!-- 连接数 -->
<a-col :span="12">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<div class="card-title">
<h3>连接数</h3>
</div>
</template>
<div class="card-content">
<a-statistic title="TCP" :value="connectionsMetrics.tcp" />
<a-statistic title="UDP" :value="connectionsMetrics.udp" />
<a-statistic title="总计" :value="connectionsMetrics.tcp + connectionsMetrics.udp" />
</div>
</a-card>
</a-col>
</a-row>
</a-col>
<!-- 第二层数据 -->
<template v-if="!renderLoading && !nodata">
<!-- 磁盘 -->
<a-col v-for="disk in diskMetrics" :span="6">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<h3>磁盘</h3>
<a-space>
<a-tag color="green">{{ disk.capacity }}</a-tag>
<a-tag color="arcoblue">name: {{ disk.name }}</a-tag>
</a-space>
</template>
<div class="card-content">
<a-statistic title="使用率"
:value="disk.usedPercent"
:precision="2"
:value-style="{ color: getPercentProgressColor(disk.usedPercent / 100, '') }">
<template #suffix>%</template>
</a-statistic>
<a-statistic title="使用量"
:value="disk.used"
:precision="2"
:value-style="{ color: getPercentProgressColor(disk.usedPercent / 100, '') }">
<template #suffix>{{ disk.usedUnit }}</template>
</a-statistic>
</div>
</a-card>
</a-col>
<!-- 磁盘吞吐量 -->
<a-col :span="6">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<h3>磁盘吞吐量</h3>
</template>
<div class="card-content">
<a-statistic title="读取/秒"
:value="ioMetrics.readBytesPerSecond"
:precision="2"
:value-style="{ color: 'rgb(var(--green-6))' }">
<template #prefix>
<icon-arrow-rise />
</template>
<template #suffix>{{ ioMetrics.readsUnit }}</template>
</a-statistic>
<a-statistic title="写入/秒"
:value="ioMetrics.writeBytesPerSecond"
:precision="2"
:value-style="{ color: 'rgb(var(--arcoblue-6))' }">
<template #prefix>
<icon-arrow-fall />
</template>
<template #suffix>{{ ioMetrics.writesUnit }}</template>
</a-statistic>
</div>
</a-card>
</a-col>
<!-- 磁盘 IOPS -->
<a-col :span="6">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<h3>磁盘 IOPS</h3>
</template>
<div class="card-content">
<a-statistic title="读取/秒"
:value="ioMetrics.readsPerSecond"
:precision="2"
:value-style="{ color: 'rgb(var(--green-6))' }">
<template #prefix>
<icon-arrow-fall />
</template>
<template #suffix></template>
</a-statistic>
<a-statistic title="写入/秒"
:value="ioMetrics.writesPerSecond"
:precision="2"
:value-style="{ color: 'rgb(var(--arcoblue-6))' }">
<template #prefix>
<icon-arrow-fall />
</template>
<template #suffix></template>
</a-statistic>
</div>
</a-card>
</a-col>
<!-- 网络 -->
<a-col v-for="network in networkMetrics" :span="6">
<a-card class="metric-card"
size="small"
:bordered="false"
:header-style="{ height: '48px', borderBottom: 'none' }"
:body-style="{ height: 'calc(100% - 48px)' }">
<template #title>
<h3>网络</h3>
<a-tag color="arcoblue">name: {{ network.name }}</a-tag>
</template>
<div class="card-content">
<a-statistic title="上行速率/秒"
:value="network.sentBytesPerSecond"
:precision="2"
:value-style="{ color: 'rgb(var(--green-6))' }">
<template #prefix>
<icon-arrow-rise />
</template>
<template #suffix>{{ network.sentUnit }}</template>
</a-statistic>
<a-statistic title="下行速率/秒"
:value="network.recvBytesPerSecond"
:precision="2"
:value-style="{ color: 'rgb(var(--arcoblue-6))' }">
<template #prefix>
</template>
<template #suffix>{{ network.recvUnit }}</template>
</a-statistic>
</div>
</a-card>
</a-col>
</template>
</a-row>
</div>
</template>
<script lang="ts">
export default {
name: 'hostOverviewTab'
};
</script>
<script lang="ts" setup>
import type { HostQueryResponse } from '@/api/asset/host';
import type { MonitorMetrics } from '@/api/monitor/monitor-host';
import { ref, onMounted } from 'vue';
import { getMonitorHostOverride } from '@/api/monitor/monitor-host';
import { getPercentProgressColor } from '@/utils/charts';
import { extractUnit, formatBytes } from '@/utils/metrics';
const props = defineProps<{
agentKey: string;
host: HostQueryResponse;
}>();
const emits = defineEmits(['setTimestamp']);
const renderLoading = ref(true);
const nodata = ref(true);
const cpuMetrics = ref({ user: 0, system: 0, total: 0 });
const memoryMetrics = ref({ used: 0, usedUnit: 'B', usedPercent: 0, total: '', swapUsed: 0, swapUsedUnit: 'B', swapUsedPercent: 0, swapTotal: '' });
const loadMetrics = ref({ load1: 0, load5: 0, load15: 0 });
const connectionsMetrics = ref({ tcp: 0, udp: 0 });
const ioMetrics = ref({ readsPerSecond: 0, readBytesPerSecond: 0, readsUnit: 'B', writesPerSecond: 0, writeBytesPerSecond: 0, writesUnit: 'B' });
const diskMetrics = ref<any[]>([]);
const networkMetrics = ref<any[]>([]);
// 重新加载
const reload = async () => {
// 加载概览信息
const { data } = await getMonitorHostOverride(props.agentKey);
if (data?.timestamp) {
emits('setTimestamp', data.timestamp);
}
if (data?.metrics?.length) {
nodata.value = false;
// 解析数据
parseData(data.metrics);
} else {
nodata.value = true;
}
};
defineExpose({ reload });
// 解析数据
const parseData = (metrics: Array<MonitorMetrics>) => {
// cpu
const cpu = metrics.find(s => s.type === 'cpu');
if (cpu) {
cpuMetrics.value = {
user: cpu.values?.cpu_user_seconds_total || 0,
system: cpu.values?.cpu_system_seconds_total || 0,
total: cpu.values?.cpu_total_seconds_total || 0,
};
}
// 内存
const memory = metrics.find(s => s.type === 'memory');
if (memory) {
const used = memory.values?.mem_used_bytes_total || 0;
const swapUsed = memory.values?.mem_swap_used_bytes_total || 0;
const usedFormat = formatBytes(used);
const usedSwapFormat = formatBytes(swapUsed);
memoryMetrics.value = {
used: Number.parseFloat(usedFormat),
usedUnit: extractUnit(usedFormat),
usedPercent: memory.values?.mem_used_percent || 0,
total: formatBytes(used / (memory.values?.mem_used_percent || 100) * 100),
swapUsed: Number.parseFloat(usedSwapFormat),
swapUsedUnit: extractUnit(usedSwapFormat),
swapUsedPercent: memory.values?.mem_swap_used_percent || 0,
swapTotal: formatBytes(swapUsed / (memory.values?.mem_swap_used_percent || 100) * 100),
};
}
// 负载
const load = metrics.find(s => s.type === 'load');
if (load) {
loadMetrics.value = {
load1: load.values?.load1 || 0,
load5: load.values?.load5 || 0,
load15: load.values?.load15 || 0,
};
}
// 连接数
const connections = metrics.find(s => s.type === 'connections');
if (connections) {
connectionsMetrics.value = {
udp: connections.values?.net_udp_connections || 0,
tcp: connections.values?.net_tcp_connections || 0,
};
}
// io
const io = metrics.find(s => s.type === 'io');
if (io) {
const readBytesPerSecond = formatBytes(io.values?.disk_io_read_bytes_per_second || 0);
const writeBytesPerSecond = formatBytes(io.values?.disk_io_write_bytes_per_second || 0);
ioMetrics.value = {
readsPerSecond: io.values?.disk_io_reads_per_second || 0,
readBytesPerSecond: Number.parseFloat(readBytesPerSecond),
readsUnit: extractUnit(readBytesPerSecond),
writesPerSecond: io.values?.disk_io_writes_per_second || 0,
writeBytesPerSecond: Number.parseFloat(writeBytesPerSecond),
writesUnit: extractUnit(writeBytesPerSecond),
};
}
// 磁盘
const disks = metrics.filter(s => s.type === 'disk');
if (disks.length) {
diskMetrics.value = disks.map(disk => {
const used = disk.values?.disk_fs_used_bytes_total || 0;
const total = used / (disk.values?.disk_fs_used_percent || 100) * 100;
const usedFormat = formatBytes(used);
const totalFormat = formatBytes(total);
return {
name: disk.tags?.['name'] || '',
used: Number.parseFloat(usedFormat),
usedUnit: extractUnit(usedFormat),
usedPercent: disk.values?.disk_fs_used_percent || 0,
capacity: totalFormat,
};
});
}
// 网卡
const networks = metrics.filter(s => s.type === 'network');
if (networks.length) {
networkMetrics.value = networks.map(network => {
const sentBytesPerSecond = network.values?.net_sent_bytes_per_second || 0;
const recvBytesPerSecond = network.values?.net_recv_bytes_per_second || 0;
const sentFormat = formatBytes(sentBytesPerSecond);
const recvFormat = formatBytes(recvBytesPerSecond);
return {
name: network.tags?.['name'] || '',
sentBytesPerSecond: Number.parseFloat(sentFormat),
sentUnit: extractUnit(sentFormat),
recvBytesPerSecond: Number.parseFloat(recvFormat),
recvUnit: extractUnit(recvFormat),
};
});
}
};
// 初始化数据
onMounted(async () => {
try {
renderLoading.value = true;
await reload();
} catch (e) {
} finally {
renderLoading.value = false;
}
});
</script>
<style lang="less" scoped>
:deep(.arco-card-header-title) {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
display: inline-block;
}
}
.host-info-card {
height: 100%;
border-radius: 8px;
:deep(.arco-card-body) {
height: 100%;
padding: 0;
}
.host-info-content {
height: 100%;
overflow-y: auto;
padding: 12px 20px 16px 20px;
}
}
.render-skeleton {
padding: 16px 24px;
border-radius: 8px;
background: var(--color-bg-2);;
}
.metric-card {
height: 180px;
border-radius: 8px;
:deep(.arco-card-body) {
height: 100%;
padding: 0;
}
}
.card-content {
height: 100%;
padding: 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
}
</style>

View File

@@ -18,7 +18,7 @@
<script lang="ts" setup>
import type { MetricsChartProps, MetricsChartOption } from '../types/const';
import { computed, ref } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { parseWindowUnit, MetricsUnit, type MetricUnitType } from '@/utils/metrics';
import { TimeSeriesColors } from '@/types/chart';
import MetricsChart from './metrics-chart.vue';
@@ -61,7 +61,7 @@
colors: [[TimeSeriesColors.BLUE.lineColor, TimeSeriesColors.BLUE.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.PER as MetricUnitType,
unitOption: { digit: 3 }
unitOption: { precision: 3 }
},
{
name: '内存使用率',
@@ -70,7 +70,7 @@
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor], [TimeSeriesColors.TEAL.lineColor, TimeSeriesColors.TEAL.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.PER as MetricUnitType,
unitOption: { digit: 3 }
unitOption: { precision: 3 }
},
{
name: '内存使用量',
@@ -79,7 +79,7 @@
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor], [TimeSeriesColors.TEAL.lineColor, TimeSeriesColors.TEAL.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.BYTES as MetricUnitType,
unitOption: { digit: 2 }
unitOption: { precision: 2 }
},
{
name: '系统负载',
@@ -91,7 +91,7 @@
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor], [TimeSeriesColors.RED.lineColor, TimeSeriesColors.RED.itemBorderColor], [TimeSeriesColors.BLUE.lineColor, TimeSeriesColors.BLUE.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.NONE as MetricUnitType,
unitOption: { digit: 2 }
unitOption: { precision: 2 }
},
{
name: '磁盘使用率',
@@ -100,7 +100,7 @@
colors: [[TimeSeriesColors.VIOLET.lineColor, TimeSeriesColors.VIOLET.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.PER as MetricUnitType,
unitOption: { digit: 2 }
unitOption: { precision: 2 }
},
{
name: '磁盘使用量',
@@ -109,7 +109,7 @@
colors: [[TimeSeriesColors.LIME.lineColor, TimeSeriesColors.LIME.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.BYTES as MetricUnitType,
unitOption: { digit: 2 }
unitOption: { precision: 2 }
},
{
name: '网络连接数',
@@ -118,7 +118,7 @@
colors: [[TimeSeriesColors.CYAN.lineColor, TimeSeriesColors.CYAN.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.COUNT as MetricUnitType,
unitOption: { digit: 0, suffix: '个' }
unitOption: { precision: 0, suffix: '个' }
},
{
name: '网络带宽',
@@ -127,7 +127,7 @@
colors: [[TimeSeriesColors.BLUE.lineColor, TimeSeriesColors.BLUE.itemBorderColor], [TimeSeriesColors.GREEN.lineColor, TimeSeriesColors.GREEN.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.BITS_S as MetricUnitType,
unitOption: { digit: 2 }
unitOption: { precision: 2 }
},
{
name: '磁盘IO',
@@ -136,7 +136,7 @@
colors: [[TimeSeriesColors.CYAN.lineColor, TimeSeriesColors.CYAN.itemBorderColor], [TimeSeriesColors.YELLOW.lineColor, TimeSeriesColors.YELLOW.itemBorderColor]],
aggregate: 'mean',
unit: MetricsUnit.BYTES_S as MetricUnitType,
unitOption: { digit: 2 }
unitOption: { precision: 2 }
},
];
return options.map(option => {
@@ -169,6 +169,8 @@
defineExpose({ reload });
onMounted(reload);
</script>
<style lang="less" scoped>

View File

@@ -112,7 +112,7 @@
if (value === undefined || value === null) {
displayValue = '-';
} else {
displayValue = MetricUnitFormatter[props.option.unit](value, props.option.unitOption);
displayValue = MetricUnitFormatter[props.option.unit].format(value, props.option.unitOption);
}
headerNames.forEach((key, index) => {
const cellValue = key === 'value' ? displayValue : (tags[key as any] || '-');
@@ -165,7 +165,7 @@
type: 'value',
axisLabel: {
color: themeTextColor,
formatter: (s: number) => MetricUnitFormatter[props.option.unit](s, props.option.unitOption)
formatter: (s: number) => MetricUnitFormatter[props.option.unit].format(s, props.option.unitOption)
},
axisLine: {
show: false,
@@ -178,7 +178,10 @@
},
legend: {
show: props.option.legend === true,
type: 'scroll'
type: 'scroll',
textStyle: {
color: themeTextColor,
}
},
series: series.value.map((s, index) => {
let colors = props.option.colors[index];

View File

@@ -7,16 +7,34 @@
v-model:activeKey="activeKey"
v-model:chartCompose="chartCompose"
:host="host"
:override-timestamp="overrideTimestamp"
@reload-chart="reloadChart" />
<!-- 内容部分 -->
<div v-if="host" class="content-container">
<!-- 监控图表 -->
<metrics-chart-tab v-show="activeKey === TabKeys.CHART"
ref="chartRef"
:agentKey="host.agentKey"
:chartCompose="chartCompose"
:chartRange="chartRange"
:chartWindow="chartWindow" />
<a-tabs v-model:active-key="activeKey"
class="main-content"
lazy-load>
<!-- 主机概览 -->
<a-tab-pane :key="TabKeys.OVERVIEW">
<host-overview-tab ref="overrideRef"
:host="host"
:agent-key="host.agentKey"
@set-timestamp="(s: number) => overrideTimestamp = s" />
</a-tab-pane>
<!-- 监控图表 -->
<a-tab-pane :key="TabKeys.CHART">
<metrics-chart-tab ref="chartRef"
:agentKey="host.agentKey"
:chartCompose="chartCompose"
:chartRange="chartRange"
:chartWindow="chartWindow" />
</a-tab-pane>
<!-- 告警列表 -->
<a-tab-pane :key="TabKeys.ALARM">
<alarm-event-tab :agentKey="host.agentKey" />
</a-tab-pane>
</a-tabs>
</div>
</a-spin>
</template>
@@ -38,14 +56,19 @@
import { parseWindowUnit, WindowUnitFormatter } from '@/utils/metrics';
import DetailHeader from './compoments/detail-header.vue';
import MetricsChartTab from './compoments/metrics-chart-tab.vue';
import AlarmEventTab from './compoments/alarm-event-tab.vue';
import HostOverviewTab from './compoments/host-overview-tab.vue';
const hostId = ref<number>();
const host = ref<HostQueryResponse>();
const activeKey = ref(TabKeys.CHART);
const activeKey = ref(TabKeys.OVERVIEW);
const chartCompose = ref(true);
const chartRef = ref();
const overrideRef = ref();
const reloadChartId = ref<number>();
const reloadOverrideId = ref<number>();
const overrideTimestamp = ref<number>(0);
const chartRange = ref<string>('-30m');
const chartWindow = ref<string>('1m');
@@ -55,13 +78,33 @@
chartWindow.value = _chartWindow;
// 立即加载和定时加载
setTimeout(() => {
chartRef.value.reload();
chartRef?.value?.reload?.();
}, 50);
// 重置定时加载表格;
resetReloadChartInterval();
};
// 重置定时加载表格
// 重置计时器
const resetInterval = () => {
// 重置定时加载概览
resetReloadOverrideInterval();
// 重置定时加载图表
resetReloadChartInterval();
};
// 重置定时加载概览
const resetReloadOverrideInterval = () => {
// 清除定时
window.clearInterval(reloadOverrideId.value);
// 重新设置定时刷新
reloadOverrideId.value = window.setInterval(() => {
if (activeKey.value === TabKeys.OVERVIEW) {
overrideRef.value.reload();
}
}, 60000);
};
// 重置定时加载图表
const resetReloadChartInterval = () => {
if (!chartWindow.value) {
return;
@@ -71,12 +114,22 @@
// 计算窗口
const [windowTime, windowUnit] = parseWindowUnit(chartWindow.value as string);
const interval = WindowUnitFormatter[windowUnit].windowInterval(windowTime) + 5000;
// 重新设置定时
// 重新设置定时刷新
reloadChartId.value = window.setInterval(() => {
chartRef.value.reload();
if (activeKey.value === TabKeys.CHART) {
chartRef.value.reload();
}
}, interval);
};
// 清除计时器
const clearInterval = () => {
// 清除定时刷新概览
window.clearInterval(reloadOverrideId.value);
// 清除定时刷新图表
window.clearInterval(reloadChartId.value);
};
onMounted(async () => {
const route = useRoute();
hostId.value = parseInt(route.query.hostId as string);
@@ -92,9 +145,9 @@
host.value = data;
});
onActivated(resetReloadChartInterval);
onDeactivated(() => window.clearInterval(reloadChartId.value));
onUnmounted(() => window.clearInterval(reloadChartId.value));
onActivated(resetInterval);
onDeactivated(clearInterval);
onUnmounted(clearInterval);
</script>
@@ -109,4 +162,14 @@
.content-container {
border-radius: 4px;
}
.main-content {
:deep(.arco-tabs-nav-tab) {
display: none;
}
:deep(.arco-tabs-content) {
padding-top: 0 !important;
}
}
</style>

View File

@@ -1,4 +1,5 @@
import type { WindowUnit, MetricUnitType, MetricUnitFormatOptions } from '@/utils/metrics';
import { dictKeys as alarmDictKeys } from '@/views/monitor/alarm-event/types/const';
// 图表组件配置
export interface MetricsChartProps {
@@ -26,9 +27,13 @@ export interface MetricsChartOption {
// tab
export const TabKeys = {
CHART: 'chart'
OVERVIEW: 'overview',
CHART: 'chart',
ALARM: 'alarm',
};
export const TableName = 'host_alarm_event';
// 探针在线状态 字典项
export const OnlineStatusKey = 'agentOnlineStatus';
@@ -42,4 +47,4 @@ export const ChartRangeKey = 'metricsChartRange';
export const MetricsAggregateKey = 'metricsAggregate';
// 加载的字典值
export const dictKeys = [AlarmSwitchKey, OnlineStatusKey, ChartRangeKey, MetricsAggregateKey];
export const dictKeys = [AlarmSwitchKey, OnlineStatusKey, ChartRangeKey, MetricsAggregateKey, ...alarmDictKeys];

View File

@@ -4,7 +4,7 @@
:create-card-position="false"
:loading="loading"
:field-config="cardFieldConfig"
:list="list"
:list="renderList"
:pagination="pagination"
:card-layout-cols="cardColLayout"
:filter-count="filterCount"
@@ -101,7 +101,7 @@
<!-- 在线状态 -->
<template #agentOnlineStatus="{ record }">
<monitor-cell :data-cell="false" :record="record">
<a-tooltip :content="'切换分区时间: ' + dateFormat(new Date(record.lastChangeOnlineTime))" mini>
<a-tooltip :content="'切换分区时间: ' + dateFormat(new Date(record.agentOnlineChangeTime))" mini>
<a-tag :color="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'color')">
<template #icon>
<component :is="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'icon')" />
@@ -184,7 +184,11 @@
<!-- 告警策略 -->
<template #alarmPolicy="{ record }">
<monitor-cell :data-cell="false" :record="record">
{{ getDictValue(AlarmSwitchKey, record.alarmSwitch) }}
<b class="pointer"
:style="{ color: record.alarmSwitch ? 'rgb(var(--green-6))' : 'rgb(var(--gray-6))' }"
@click="emits('toPolicy', record)">
{{ record.policyName || '-' }}
</b>
</monitor-cell>
</template>
<!-- 告警负责人 -->
@@ -334,7 +338,7 @@
import MonitorCell from './monitor-cell.vue';
import UserSelector from '@/components/user/user/selector/index.vue';
const emits = defineEmits(['openUpdate', 'openUpload']);
const emits = defineEmits(['openUpdate', 'openUpload', 'toPolicy']);
const cardColLayout = useCardColLayout();
const pagination = useCardPagination();
@@ -342,7 +346,7 @@
const { cardFieldConfig, fieldsHook } = useCardFieldConfig(TableName, fieldConfig);
const { toOptions, getDictValue, toggleDictValue } = useDictStore();
const list = ref<Array<MonitorHostQueryResponse>>([]);
const renderList = ref<Array<MonitorHostQueryResponse>>([]);
const formRef = ref();
const formModel = reactive<MonitorHostQueryRequest>({
searchValue: undefined,
@@ -379,7 +383,7 @@
setInstallSuccess,
toggleAlarmSwitch,
} = useMonitorHostList({
hosts: list,
hosts: renderList,
setLoading,
reload,
});
@@ -395,7 +399,7 @@
try {
setLoading(true);
const { data } = await getMonitorHostPage(request);
list.value = data.rows;
renderList.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;

View File

@@ -28,6 +28,12 @@
placeholder="请选择负责人"
allow-clear />
</a-form-item>
<!-- 告警策略 -->
<a-form-item field="policyId" label="告警策略">
<alarm-policy-selector v-model="formModel.policyId"
placeholder="请选择告警策略"
allow-clear />
</a-form-item>
<!-- 告警开关 -->
<a-form-item field="alarmSwitch"
label="告警开关"
@@ -84,6 +90,7 @@
import { updateMonitorHost } from '@/api/monitor/monitor-host';
import { Message } from '@arco-design/web-vue';
import UserSelector from '@/components/user/user/selector/index.vue';
import AlarmPolicySelector from '@/components/monitor/alarm-policy/selector/index.vue';
const emits = defineEmits(['updated']);

View File

@@ -62,6 +62,28 @@
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 开启告警 -->
<a-button v-if="selectedKeys.length"
v-permission="['monitor:monitor-host:update', 'monitor:monitor-host:update-switch']"
type="primary"
status="success"
@click="toggleAlarmSwitchBatch(selectedKeys, AlarmSwitch.ON)">
开启告警
<template #icon>
<icon-play-arrow-fill />
</template>
</a-button>
<!-- 关闭告警 -->
<a-button v-if="selectedKeys.length"
v-permission="['monitor:monitor-host:update', 'monitor:monitor-host:update-switch']"
type="primary"
status="warning"
@click="toggleAlarmSwitchBatch(selectedKeys, AlarmSwitch.OFF)">
关闭告警
<template #icon>
<icon-pause />
</template>
</a-button>
<!-- 安装 -->
<a-button v-permission="['asset:host:install-agent']"
type="primary"
@@ -132,7 +154,7 @@
<!-- 在线状态 -->
<template #agentOnlineStatus="{ record }">
<monitor-cell :data-cell="false" :record="record">
<a-tooltip :content="'切换分区时间: ' + dateFormat(new Date(record.lastChangeOnlineTime))" mini>
<a-tooltip :content="'切换分区时间: ' + dateFormat(new Date(record.agentOnlineChangeTime))" mini>
<a-tag :color="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'color')">
<template #icon>
<component :is="getDictValue(OnlineStatusKey, record.agentOnlineStatus, 'icon')" />
@@ -214,7 +236,11 @@
<!-- 告警策略 -->
<template #alarmPolicy="{ record }">
<monitor-cell :data-cell="false" :record="record">
{{ getDictValue(AlarmSwitchKey, record.alarmSwitch) }}
<b class="pointer"
:style="{ color: record.alarmSwitch ? 'rgb(var(--green-6))' : 'rgb(var(--gray-6))' }"
@click="emits('toPolicy', record)">
{{ record.policyName || '-' }}
</b>
</monitor-cell>
</template>
<!-- 告警负责人 -->
@@ -350,10 +376,10 @@
<script lang="ts" setup>
import type { MonitorHostQueryRequest, MonitorHostQueryResponse } from '@/api/monitor/monitor-host';
import { reactive, ref, onMounted } from 'vue';
import { getMonitorHostPage } from '@/api/monitor/monitor-host';
import { getMonitorHostPage, updateMonitorHostAlarmSwitch } from '@/api/monitor/monitor-host';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { TableName, AlarmSwitchKey, OnlineStatusKey, InstallStatusKey, AgentLogStatus, AgentLogStatusKey } from '../types/const';
import { TableName, AlarmSwitch, AlarmSwitchKey, OnlineStatusKey, InstallStatusKey, AgentLogStatus, AgentLogStatusKey } from '../types/const';
import { AgentInstallStatus, tagColor } from '@/views/asset/host-list/types/const';
import { useTablePagination, useTableColumns, useRowSelection } from '@/hooks/table';
import { useDictStore } from '@/store';
@@ -361,12 +387,13 @@
import { getPercentProgressColor } from '@/utils/charts';
import { getFileSize } from '@/utils/file';
import { dateFormat, dataColor } from '@/utils';
import { Message, Modal } from '@arco-design/web-vue';
import useMonitorHostList from '../types/use-monitor-host-list';
import MonitorCell from './monitor-cell.vue';
import TableAdjust from '@/components/app/table-adjust/index.vue';
import UserSelector from '@/components/user/user/selector/index.vue';
const emits = defineEmits(['openUpdate', 'openUpload']);
const emits = defineEmits(['openUpdate', 'openUpload', 'toPolicy']);
const rowSelection = useRowSelection();
const pagination = useTablePagination();
@@ -415,7 +442,35 @@
if (record.agentInstallStatus === AgentInstallStatus.NOT_INSTALL) {
return 'not-install';
}
return '';
return 'installed';
};
// 批量修改告警开关状态
const toggleAlarmSwitchBatch = async (hostIdList: Array<number>, alarmSwitch: number) => {
const label = getDictValue(AlarmSwitchKey, alarmSwitch);
Modal.confirm({
title: `${label}确认`,
titleAlign: 'start',
content: `确定要${label}告警功能吗?`,
okText: '确定',
onOk: async () => {
try {
setLoading(true);
const rows = tableRenderData.value.filter(s => hostIdList.includes(s.hostId));
if (!rows.length) {
return;
}
const idList = rows.map(s => s.id).filter(Boolean);
// 调用修改接口
await updateMonitorHostAlarmSwitch({ idList, alarmSwitch });
rows.forEach(s => s.alarmSwitch = alarmSwitch);
Message.success(`已${label}`);
} catch (e) {
} finally {
setLoading(false);
}
}
});
};
// 加载数据

View File

@@ -3,11 +3,13 @@
<!-- 列表-表格 -->
<monitor-host-table v-if="renderTable"
ref="table"
@to-policy="toPolicy"
@open-upload="() => uploadModal.open()"
@open-update="(e: any) => drawer.openUpdate(e)" />
<!-- 列表-卡片 -->
<monitor-host-card-list v-else
ref="card"
@to-policy="toPolicy"
@open-upload="() => uploadModal.open()"
@open-update="(e: any) => drawer.openUpdate(e)" />
<!-- 添加修改抽屉 -->
@@ -29,11 +31,13 @@
import { computed, ref, onBeforeMount } from 'vue';
import { useAppStore, useDictStore } from '@/store';
import { dictKeys } from './types/const';
import { useRouter } from 'vue-router';
import MonitorHostTable from './components/monitor-host-table.vue';
import MonitorHostCardList from './components/monitor-host-card-list.vue';
import MonitorHostFormDrawer from './components/monitor-host-form-drawer.vue';
import ReleaseUploadModal from './components/release-upload-modal.vue';
const router = useRouter();
const appStore = useAppStore();
const renderTable = computed(() => appStore.monitorHostView === 'table');
@@ -53,6 +57,20 @@
}
};
// 打开规则
const toPolicy = (e: any) => {
if (!e.policyId) {
return;
}
router.push({
name: 'alarmRule',
query: {
id: e.policyId,
name: e.policyName,
}
});
};
onBeforeMount(async () => {
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);

View File

@@ -26,6 +26,14 @@ const fieldConfig = {
slotName: 'ownerUsername',
ellipsis: true,
default: true,
}, {
label: '告警策略',
dataIndex: 'alarmPolicy',
slotName: 'alarmPolicy',
align: 'left',
ellipsis: true,
width: 140,
default: true,
}, {
label: '设备状态',
dataIndex: 'agentOnlineStatus',

View File

@@ -1,16 +1,14 @@
import type { FieldRule } from '@arco-design/web-vue';
export const policyId = [{
required: false,
message: '请输入策略id'
}] as FieldRule[];
export const alarmSwitch = [{
required: true,
message: '请输入告警开关'
}] as FieldRule[];
export default {
policyId,
alarmSwitch,
const rules = {
policyId: [{
required: false,
message: '请输入策略id'
}],
alarmSwitch: [{
required: true,
message: '请输入告警开关'
}],
} as Record<string, FieldRule | FieldRule[]>;
export default rules;

View File

@@ -17,6 +17,14 @@ const columns = [
align: 'left',
fixed: 'left',
default: true,
}, {
title: '告警策略',
dataIndex: 'alarmPolicy',
slotName: 'alarmPolicy',
align: 'left',
ellipsis: true,
width: 130,
default: true,
}, {
title: '设备状态',
dataIndex: 'agentOnlineStatus',
@@ -74,14 +82,6 @@ const columns = [
width: 288,
default: false,
}, {
// TODO
// title: '告警策略',
// dataIndex: 'alarmPolicy',
// slotName: 'alarmPolicy',
// align: 'left',
// width: 120,
// default: true,
// }, {
title: '负责人',
dataIndex: 'ownerUsername',
slotName: 'ownerUsername',

View File

@@ -121,7 +121,7 @@ export default function useMonitorHostList(options: UseMonitorHostListOptions) {
const newSwitch = dict.value as number;
// 调用修改接口
await updateMonitorHostAlarmSwitch({
id: record.id,
idList: [record.id],
alarmSwitch: newSwitch,
});
record.alarmSwitch = newSwitch;

View File

@@ -14,6 +14,7 @@ const columns = [
title: '配置项',
dataIndex: 'keyName',
slotName: 'keyName',
minWidth: 158,
align: 'left',
ellipsis: true,
tooltip: true,
@@ -22,6 +23,7 @@ const columns = [
title: '配置描述',
dataIndex: 'description',
slotName: 'description',
minWidth: 188,
align: 'left',
ellipsis: true,
tooltip: true,

View File

@@ -14,6 +14,7 @@ const columns = [
title: '配置项',
dataIndex: 'keyName',
slotName: 'keyName',
minWidth: 158,
align: 'left',
ellipsis: true,
tooltip: true,
@@ -22,6 +23,7 @@ const columns = [
title: '配置描述',
dataIndex: 'label',
slotName: 'label',
minWidth: 158,
align: 'left',
ellipsis: true,
tooltip: true,
@@ -30,6 +32,7 @@ const columns = [
title: '配置值',
dataIndex: 'value',
slotName: 'value',
minWidth: 158,
align: 'left',
ellipsis: true,
default: true,

View File

@@ -0,0 +1,235 @@
<template>
<a-drawer v-model:visible="visible"
:title="title"
:width="540"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@cancel="handleClose">
<a-spin class="full drawer-form-small" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="formRules">
<!-- 通知名称 -->
<a-form-item field="name" label="通知名称">
<a-input v-model="formModel.name"
placeholder="请输入通知名称"
allow-clear />
</a-form-item>
<!-- 业务类型 -->
<a-form-item field="bizType"
label="业务类型"
:disabled="formHandle === 'add'">
<a-select v-model="formModel.bizType"
:options="toOptions(BizTypeKey)"
placeholder="请选择业务类型"
allow-clear />
</a-form-item>
<!-- 渠道类型 -->
<a-form-item field="channelType" label="渠道类型">
<a-select v-model="formModel.channelType"
:options="toOptions(ChannelTypeKey)"
placeholder="请选择渠道类型"
allow-clear />
</a-form-item>
<!-- 消息分类 -->
<a-form-item v-if="formModel.channelType === ChannelType.WEBSITE"
field="messageClassify"
label="消息分类">
<a-select v-model="formModel.messageClassify"
:options="toOptions(messageClassifyKey)"
placeholder="请选择消息分类"
allow-clear />
</a-form-item>
<!-- 消息类型 -->
<a-form-item v-if="formModel.channelType === ChannelType.WEBSITE"
field="messageType"
label="消息类型">
<a-select v-model="formModel.messageType"
:options="toOptions(messageTypeKey)"
placeholder="请选择消息分类"
allow-clear />
</a-form-item>
<!-- webhook -->
<a-form-item v-if="formModel.channelType && getDictValue(ChannelTypeKey, formModel.channelType, 'notifyType') === NotifyType.WEBHOOK"
field="webhook"
label="webhook">
<a-textarea v-model="formModel.webhook"
:auto-size="{ minRows: 3, maxRows: 3}"
placeholder="请输入 webhook 地址"
allow-clear />
</a-form-item>
<!-- 签名密钥 -->
<a-form-item v-if="formModel.channelType === ChannelType.DING || formModel.channelType === ChannelType.FEI_SHU"
field="secret"
label="签名密钥">
<a-input v-model="formModel.secret"
placeholder="请输入签名密钥"
allow-clear />
</a-form-item>
<!-- 消息标题 -->
<a-form-item v-if="formModel.channelType === ChannelType.WEBSITE
|| formModel.channelType === ChannelType.DING"
field="title"
label="消息标题"
extra="标题同样支持变量">
<a-input v-model="formModel.title"
placeholder="请输入消息标题"
allow-clear />
</a-form-item>
<!-- 消息模板 -->
<a-form-item field="messageTemplate"
label="消息模板"
:extra="formModel.channelType ? getDictValue(ChannelTypeKey, formModel.channelType, 'templateTips') : ''">
<a-textarea v-model="formModel.messageTemplate"
:auto-size="{ minRows: 10, maxRows: 10 }"
placeholder="请输入消息模板"
allow-clear />
</a-form-item>
<!-- 通知描述 -->
<a-form-item field="description" label="通知描述">
<a-textarea v-model="formModel.description"
:auto-size="{ minRows: 3, maxRows: 3}"
placeholder="请输入通知描述"
allow-clear />
</a-form-item>
</a-form>
</a-spin>
</a-drawer>
</template>
<script lang="ts">
export default {
name: 'notifyTemplateFormDrawer'
};
</script>
<script lang="ts" setup>
import type { FormHandle } from '@/types/form';
import type { NotifyTemplateUpdateRequest } from '@/api/system/notify-template';
import type { NotifyTemplateConfig } from '../types/const';
import { messageClassifyKey, messageTypeKey } from '../types/const';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/form.rules';
import { assignOmitRecord } from '@/utils';
import { BizTypeKey, ChannelTypeKey, BizType, ChannelType } from '../types/const';
import { createNotifyTemplate, updateNotifyTemplate } from '@/api/system/notify-template';
import { NotifyType } from '../types/const';
import { Message } from '@arco-design/web-vue';
import { useDictStore } from '@/store';
const emits = defineEmits(['added', 'updated']);
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue } = useDictStore();
const title = ref<string>();
const formHandle = ref<FormHandle>('add');
const formRef = ref<any>();
const formModel = ref<Partial<NotifyTemplateUpdateRequest & NotifyTemplateConfig>>({});
const defaultForm = (): Partial<NotifyTemplateUpdateRequest & NotifyTemplateConfig> => {
return {
id: undefined,
name: undefined,
bizType: BizType.ALARM,
channelType: ChannelType.WEBSITE,
channelConfig: undefined,
messageTemplate: undefined,
messageClassify: 'ALARM',
messageType: 'ALARM',
};
};
// 打开新增
const openAdd = () => {
title.value = '添加通知模板';
formHandle.value = 'add';
renderForm({ ...defaultForm() });
setVisible(true);
};
// 打开复制
const openCopy = (record: any) => {
title.value = '添加通知模板';
formHandle.value = 'add';
renderForm({ ...defaultForm(), ...record, id: undefined });
setVisible(true);
};
// 打开修改
const openUpdate = (record: any) => {
title.value = '修改通知模板';
formHandle.value = 'update';
renderForm({ ...defaultForm(), ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: any) => {
const model = assignOmitRecord(record, 'channelConfig');
const channelConfig = record.channelConfig ? JSON.parse(record.channelConfig) : {};
formModel.value = { ...model, ...channelConfig };
};
defineExpose({ openAdd, openCopy, openUpdate });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
// 修改渠道配置
const channelConfig = {
webhook: formModel.value.webhook,
secret: formModel.value.secret,
title: formModel.value.title,
messageClassify: formModel.value.messageClassify,
messageType: formModel.value.messageType,
};
if (formHandle.value === 'add') {
// 新增
await createNotifyTemplate({ ...formModel.value, channelConfig: JSON.stringify(channelConfig) });
Message.success('创建成功');
emits('added');
} else {
// 修改
await updateNotifyTemplate({ ...formModel.value, channelConfig: JSON.stringify(channelConfig) });
Message.success('修改成功');
emits('updated');
}
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,268 @@
<template>
<!-- 搜索 -->
<a-card class="general-card table-search-card">
<query-header :model="formModel"
label-align="left"
@submit="fetchTableData"
@reset="fetchTableData"
@keyup.enter="() => fetchTableData()">
<!-- id -->
<a-form-item field="id" label="id">
<a-input-number v-model="formModel.id"
placeholder="请输入id"
hide-button
allow-clear />
</a-form-item>
<!-- 通知名称 -->
<a-form-item field="name" label="通知名称">
<a-input v-model="formModel.name"
placeholder="请输入通知名称"
allow-clear />
</a-form-item>
<!-- 渠道类型 -->
<a-form-item field="channelType" label="渠道类型">
<a-select v-model="formModel.channelType"
:options="toOptions(ChannelTypeKey)"
placeholder="请选择渠道类型"
allow-clear />
</a-form-item>
<!-- 通知描述 -->
<a-form-item field="name" label="通知描述">
<a-input v-model="formModel.name"
placeholder="请输入通知描述"
allow-clear />
</a-form-item>
</query-header>
</a-card>
<!-- 内容部分 -->
<div class="container-content">
<!-- 业务类型 -->
<a-card class="general-card table-search-card biz-card">
<a-tabs v-model:active-key="bizType"
direction="vertical"
type="rounded"
:hide-content="true">
<a-tab-pane v-for="item in toOptions(BizTypeKey)"
:key="item.value as string"
:title="item.label" />
</a-tabs>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧操作 -->
<div class="table-left-bar-handle">
<!-- 标题 -->
<div class="table-title">
通知模板列表
</div>
</div>
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 新增 -->
<a-button v-permission="['infra:notify-template:create']"
type="primary"
@click="emits('openAdd')">
新增
<template #icon>
<icon-plus />
</template>
</a-button>
<!-- 调整 -->
<table-adjust :columns="columns"
:columns-hook="columnsHook"
:query-order="queryOrder"
@query="fetchTableData" />
</a-space>
</div>
</template>
<!-- table -->
<a-table row-key="id"
ref="tableRef"
:loading="loading"
:columns="tableColumns"
:data="tableRenderData"
:pagination="pagination"
:bordered="false"
@page-change="(page: number) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size: number) => fetchTableData(1, size)">
<!-- 渠道类型 -->
<template #channelType="{ record }">
<a-tag :color="getDictValue(ChannelTypeKey, record.channelType, 'color')">
{{ getDictValue(ChannelTypeKey, record.channelType) }}
</a-tag>
</template>
<!-- 消息标识 -->
<template #messageTag="{ record }">
<!-- webhook -->
<span v-if="getDictValue(ChannelTypeKey, record.channelType, 'notifyType') === NotifyType.WEBHOOK"
class="text-copy"
@click="copy(extraWebhook(record), true)">
{{ extraWebhook(record) }}
</span>
<!-- 站内信 -->
<span v-else-if="getDictValue(ChannelTypeKey, record.channelType, 'notifyType') === NotifyType.WEBSITE">
<component :is="extraWebsite(record)" />
</span>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 修改 -->
<a-button v-permission="['infra:notify-template:update']"
type="text"
size="mini"
@click="emits('openUpdate', record)">
修改
</a-button>
<!-- 复制 -->
<a-button v-permission="['infra:notify-template:create']"
type="text"
size="mini"
@click="emits('openCopy', record)">
复制
</a-button>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
position="left"
type="warning"
@ok="deleteRow(record)">
<a-button v-permission="['infra:notify-template:delete']"
type="text"
size="mini"
status="danger">
删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</a-card>
</div>
</template>
<script lang="ts">
export default {
name: 'notifyTemplateTable'
};
</script>
<script lang="ts" setup>
import type { NotifyTemplateQueryRequest, NotifyTemplateQueryResponse } from '@/api/system/notify-template';
import { h, reactive, ref, onMounted } from 'vue';
import { deleteNotifyTemplate, getNotifyTemplatePage } from '@/api/system/notify-template';
import { Message, Tag } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { copy } from '@/hooks/copy';
import { TableName, BizTypeKey, ChannelTypeKey, BizType, NotifyType, messageClassifyKey, messageTypeKey } from '../types/const';
import { useTablePagination, useTableColumns } from '@/hooks/table';
import { useDictStore } from '@/store';
import { useQueryOrder, ASC } from '@/hooks/query-order';
import TableAdjust from '@/components/app/table-adjust/index.vue';
const emits = defineEmits(['openAdd', 'openUpdate', 'openCopy']);
const pagination = useTablePagination();
const { loading, setLoading } = useLoading();
const queryOrder = useQueryOrder(TableName, ASC);
const { tableColumns, columnsHook } = useTableColumns(TableName, columns);
const { toOptions, getDictValue } = useDictStore();
const bizType = ref<string>(BizType.ALARM);
const tableRenderData = ref<Array<NotifyTemplateQueryResponse>>([]);
const formModel = reactive<NotifyTemplateQueryRequest>({
id: undefined,
name: undefined,
channelType: undefined,
});
// 提取 webhook
const extraWebhook = (record: NotifyTemplateQueryResponse) => {
try {
return JSON.parse(record.channelConfig).webhook;
} catch (e) {
return '';
}
};
// 提取 website
const extraWebsite = (record: NotifyTemplateQueryResponse) => {
try {
const parse = JSON.parse(record.channelConfig);
return h('div', {}, [
h(Tag, { style: { 'margin-right': '8px' }, color: 'green' }, { default: () => getDictValue(messageClassifyKey, parse.messageClassify) }),
h(Tag, { style: { 'margin-right': '8px' }, color: 'green' }, { default: () => getDictValue(messageTypeKey, parse.messageType) }),
h(Tag, { color: 'purple' }, { default: () => parse.title }),
]);
} catch (e) {
return h('span', {}, '');
}
};
// 删除当前行
const deleteRow = async (record: NotifyTemplateQueryResponse) => {
try {
setLoading(true);
// 调用删除接口
await deleteNotifyTemplate(record.id);
Message.success('删除成功');
// 重新加载
reload();
} catch (e) {
} finally {
setLoading(false);
}
};
// 重新加载
const reload = () => {
// 重新加载数据
fetchTableData();
};
defineExpose({ reload });
// 加载数据
const doFetchTableData = async (request: NotifyTemplateQueryRequest) => {
try {
setLoading(true);
const { data } = await getNotifyTemplatePage(queryOrder.markOrderly({ ...request, bizType: bizType.value }));
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
onMounted(() => {
fetchTableData();
});
</script>
<style lang="less" scoped>
@biz-card-width: 120px;
.container-content {
display: flex;
}
.biz-card {
width: @biz-card-width;
margin: 0 16px 0 0 !important;
user-select: none;
}
.table-card {
width: calc(100% - @biz-card-width - 16px);
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="layout-container" v-if="render">
<!-- 列表-表格 -->
<notify-template-table ref="table"
@open-add="() => drawer.openAdd()"
@open-copy="(e: any) => drawer.openCopy(e)"
@open-update="(e: any) => drawer.openUpdate(e)" />
<!-- 添加修改抽屉 -->
<notify-template-form-drawer ref="drawer"
@added="reload"
@updated="reload" />
</div>
</template>
<script lang="ts">
export default {
name: 'notifyTemplate'
};
</script>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue';
import { useDictStore } from '@/store';
import { dictKeys } from './types/const';
import NotifyTemplateTable from './components/notify-template-table.vue';
import NotifyTemplateFormDrawer from './components/notify-template-form-drawer.vue';
const render = ref(false);
const table = ref();
const drawer = ref();
// 重新加载
const reload = () => {
table.value.reload();
};
onBeforeMount(async () => {
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
render.value = true;
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,44 @@
export const TableName = 'notify_template';
// 通知模板表单
export interface NotifyTemplateConfig {
webhook: string;
secret: string;
title: string;
messageClassify: string;
messageType: string;
}
// 通知业务类型
export const BizType = {
ALARM: 'ALARM',
};
// 通知渠道类型
export const ChannelType = {
WEBSITE: 'WEBSITE',
DING: 'DING',
FEI_SHU: 'FEI_SHU',
WE_COM: 'WE_COM',
};
// 通知类型
export const NotifyType = {
WEBHOOK: 'webhook',
WEBSITE: 'website',
};
// 通知业务类型 字典项
export const BizTypeKey = 'notifyBizType';
// 通知渠道类型 字典项
export const ChannelTypeKey = 'notifyChannelType';
// 消息分类 字典项
export const messageClassifyKey = 'messageClassify';
// 消息类型 字典项
export const messageTypeKey = 'messageType';
// 加载的字典值
export const dictKeys = [BizTypeKey, ChannelTypeKey, messageClassifyKey, messageTypeKey];

View File

@@ -0,0 +1,54 @@
import type { FieldRule } from '@arco-design/web-vue';
// 通知模板配置
const channelConfig = {
webhook: [{
required: true,
message: '请输入 webhook'
}],
title: [{
required: true,
message: '请输入消息标题'
}],
messageClassify: [{
required: true,
message: '请选择消息分类'
}],
messageType: [{
required: true,
message: '请选择消息类型'
}],
};
export default {
name: [{
required: true,
message: '请输入通知名称'
}, {
maxLength: 32,
message: '通知名称长度不能大于32位'
}],
bizType: [{
required: true,
message: '请输入业务类型'
}, {
maxLength: 12,
message: '业务类型长度不能大于12位'
}],
channelType: [{
required: true,
message: '请输入渠道类型'
}, {
maxLength: 12,
message: '渠道类型长度不能大于12位'
}],
messageTemplate: [{
required: true,
message: '请输入消息模板'
}],
description: [{
maxLength: 255,
message: '描述长度不能大于255位'
}],
...channelConfig,
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -0,0 +1,89 @@
import type { TableColumnData } from '@arco-design/web-vue';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 68,
align: 'left',
fixed: 'left',
default: true,
}, {
title: '通知名称',
dataIndex: 'name',
slotName: 'name',
align: 'left',
width: 168,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '渠道类型',
dataIndex: 'channelType',
slotName: 'channelType',
align: 'left',
width: 128,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '消息标识',
dataIndex: 'messageTag',
slotName: 'messageTag',
align: 'left',
width: 468,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '通知描述',
dataIndex: 'description',
slotName: 'description',
align: 'left',
minWidth: 168,
ellipsis: true,
tooltip: true,
default: true,
}, {
title: '创建时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
}, {
title: '修改时间',
dataIndex: 'updateTime',
slotName: 'updateTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.updateTime));
},
default: true,
}, {
title: '创建人',
width: 148,
dataIndex: 'creator',
slotName: 'creator',
}, {
title: '修改人',
width: 148,
dataIndex: 'updater',
slotName: 'updater',
default: true,
}, {
title: '操作',
slotName: 'handle',
width: 168,
align: 'center',
fixed: 'right',
default: true,
},
] as TableColumnData[];
export default columns;

View File

@@ -13,7 +13,9 @@
"@/*": ["src/*"]
},
"lib": ["es2021", "dom"],
"skipLibCheck": true
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"importsNotUsedAsValues": "error"
},
"include": ["src/**/*", "src/**/*.vue"],
"exclude": ["node_modules"]