🔨 前端升级.
This commit is contained in:
129
orion-visor-ui/src/api/monitor/alarm-event.ts
Normal file
129
orion-visor-ui/src/api/monitor/alarm-event.ts
Normal 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);
|
||||
}
|
||||
92
orion-visor-ui/src/api/monitor/alarm-policy.ts
Normal file
92
orion-visor-ui/src/api/monitor/alarm-policy.ts
Normal 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 } });
|
||||
}
|
||||
84
orion-visor-ui/src/api/monitor/alarm-rule.ts
Normal file
84
orion-visor-ui/src/api/monitor/alarm-rule.ts
Normal 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 } });
|
||||
}
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部监控指标
|
||||
*/
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询监控主机图表
|
||||
*/
|
||||
|
||||
91
orion-visor-ui/src/api/system/notify-template.ts
Normal file
91
orion-visor-ui/src/api/system/notify-template.ts
Normal 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 } });
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export interface OperatorLogQueryRequest extends Pagination, OrderDirection {
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志清理参数
|
||||
* 操作日志清空请求
|
||||
*/
|
||||
export interface OperatorLogClearRequest extends OperatorLogQueryRequest, ClearRequest {
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
22
orion-visor-ui/src/store/modules/cache/index.ts
vendored
22
orion-visor-ui/src/store/modules/cache/index.ts
vendored
@@ -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, {});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -239,6 +239,10 @@ export function replaceHtmlTag(message: string) {
|
||||
.replaceAll('<sr 2>', '<span class="span-red mx2">')
|
||||
.replaceAll('<sr 4>', '<span class="span-red mx4">')
|
||||
.replaceAll('</sr>', '</span>')
|
||||
.replaceAll('<sg>', '<span class="span-green mx0">')
|
||||
.replaceAll('<sg 2>', '<span class="span-green mx2">')
|
||||
.replaceAll('<sg 4>', '<span class="span-green mx4">')
|
||||
.replaceAll('</sg>', '</span>')
|
||||
.replaceAll('<b>', '<b>')
|
||||
.replaceAll('</b>', '</b>');
|
||||
}
|
||||
@@ -256,9 +260,24 @@ export function clearHtmlTag(message: string) {
|
||||
.replaceAll('<sr 2>', '')
|
||||
.replaceAll('<sr>', '')
|
||||
.replaceAll('</sr>', '')
|
||||
.replaceAll('<sg 0>', '')
|
||||
.replaceAll('<sg 2>', '')
|
||||
.replaceAll('<sg>', '')
|
||||
.replaceAll('</sg>', '')
|
||||
.replaceAll('<b>', '')
|
||||
.replaceAll('</b>', '')
|
||||
.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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const columns = [
|
||||
title: '模板命令',
|
||||
dataIndex: 'command',
|
||||
slotName: 'command',
|
||||
minWidth: 380,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
default: true,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
48
orion-visor-ui/src/views/monitor/alarm-event/index.vue
Normal file
48
orion-visor-ui/src/views/monitor/alarm-event/index.vue
Normal 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>
|
||||
30
orion-visor-ui/src/views/monitor/alarm-event/types/const.ts
Normal file
30
orion-visor-ui/src/views/monitor/alarm-event/types/const.ts
Normal 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];
|
||||
@@ -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[]>;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
43
orion-visor-ui/src/views/monitor/alarm-policy/index.vue
Normal file
43
orion-visor-ui/src/views/monitor/alarm-policy/index.vue
Normal 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>
|
||||
@@ -0,0 +1 @@
|
||||
export const TableName = 'monitor_alarm_policy';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
48
orion-visor-ui/src/views/monitor/alarm-rule/index.vue
Normal file
48
orion-visor-ui/src/views/monitor/alarm-rule/index.vue
Normal 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>
|
||||
32
orion-visor-ui/src/views/monitor/alarm-rule/types/const.ts
Normal file
32
orion-visor-ui/src/views/monitor/alarm-rule/types/const.ts
Normal 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];
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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('创建成功');
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
47
orion-visor-ui/src/views/system/notify-template/index.vue
Normal file
47
orion-visor-ui/src/views/system/notify-template/index.vue
Normal 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>
|
||||
@@ -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];
|
||||
@@ -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[]>;
|
||||
@@ -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;
|
||||
@@ -13,7 +13,9 @@
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": ["es2021", "dom"],
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"importsNotUsedAsValues": "error"
|
||||
},
|
||||
"include": ["src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user