Initial commit
This commit is contained in:
@@ -0,0 +1,650 @@
|
||||
<?php
|
||||
session_start();
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
|
||||
$expire = 1800;
|
||||
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
$_SESSION['login_time'] = time();
|
||||
$userName = htmlspecialchars($_SESSION['username'] ?? "");
|
||||
$userRole = $_SESSION['role'] ?? "user";
|
||||
|
||||
// 固定配置
|
||||
$grafanaUrl = "http://10.150.117.190:3000";
|
||||
$currentLang = "zh";
|
||||
|
||||
$langMap = [
|
||||
'zh' => [
|
||||
'page_title' => '告警时序监控面板',
|
||||
'global_search' => '全局检索(告警/实例/级别)',
|
||||
'filter_instance' => '实例IP模糊匹配',
|
||||
'start_date' => '起始日期',
|
||||
'end_date' => '结束日期',
|
||||
'severity_title' => '告警级别',
|
||||
'status_title' => '告警状态',
|
||||
'btn_query' => '查询',
|
||||
'btn_reset' => '重置筛选',
|
||||
'btn_detail_mode' => '切换详细模式',
|
||||
'tip_distinct' => '当前:去重汇总模式',
|
||||
'tip_raw' => '当前:逐条时序模式',
|
||||
'stat_total' => '总告警种类',
|
||||
'stat_firing' => '触发中',
|
||||
'stat_resolved' => '已恢复',
|
||||
'stat_critical' => '严重告警',
|
||||
'hour_chart_title' => '24小时告警触发趋势',
|
||||
'pie_title' => '告警级别分布占比',
|
||||
'col_state' => '状态',
|
||||
'col_alert' => '告警名称',
|
||||
'col_instance' => '实例地址',
|
||||
'col_severity' => '级别',
|
||||
'col_count' => '当日触发次数',
|
||||
'col_time' => '最新触发时间',
|
||||
'btn_copy_name' => '复制告警名',
|
||||
'btn_copy_link' => '复制筛选链接',
|
||||
'btn_export' => '导出CSV报表',
|
||||
'btn_refresh_auto' => '60秒自动刷新',
|
||||
'modal_title' => '告警完整详情',
|
||||
'modal_grafana' => '跳转Grafana大盘',
|
||||
'modal_labels' => '全量标签信息',
|
||||
'empty_tip' => '暂无匹配告警数据,请调整筛选条件',
|
||||
'storm_tip' => '风暴告警',
|
||||
'shortcut_tip' => 'Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索'
|
||||
]
|
||||
];
|
||||
$t = $langMap[$currentLang];
|
||||
$today = date("Y-m-d");
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo $t['page_title']; ?></title>
|
||||
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
|
||||
<script src="/static/js/jquery.min.js"></script>
|
||||
<script src="/static/js/chart.js"></script>
|
||||
<style>
|
||||
/* 全局企业化基础样式 */
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#F5F7FA;font-family:"Microsoft YaHei",system-ui,sans-serif;color:#1D2129;font-size:14px;}
|
||||
.container{max-width:1400px;margin:0 auto;padding:20px;}
|
||||
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
|
||||
.back-link{color:#165DFF;text-decoration:none;font-size:14px;}
|
||||
.user-info{font-size:14px;color:#4E5969;}
|
||||
|
||||
/* 卡片通用 */
|
||||
.card{background:#FFFFFF;border-radius:8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.06);margin-bottom:16px;}
|
||||
.card-title{font-size:15px;font-weight:500;color:#1D2129;margin-bottom:14px;}
|
||||
|
||||
/* 筛选区域 */
|
||||
.filter-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin-bottom:16px;}
|
||||
.filter-full{width:100%;}
|
||||
.filter-item{display:flex;flex-direction:column;gap:6px;min-width:160px;}
|
||||
label{font-size:13px;color:#4E5969;}
|
||||
input[type="date"],input[type="text"]{height:36px;padding:0 12px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;color:#1D2129;}
|
||||
.check-group{display:flex;gap:12px;flex-wrap:wrap;}
|
||||
.check-group label{display:flex;align-items:center;gap:4px;cursor:pointer;}
|
||||
|
||||
/* 按钮规范 */
|
||||
.btn{height:36px;padding:0 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:0.15s;}
|
||||
.btn-primary{background:#165DFF;color:#fff;}
|
||||
.btn-primary:hover{background:#0E4BDB;}
|
||||
.btn-warning{background:#FF7D00;color:#fff;}
|
||||
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
|
||||
.btn-outline:hover{border-color:#165DFF;color:#165DFF;}
|
||||
.btn-sm{height:28px;padding:0 8px;font-size:13px;}
|
||||
|
||||
/* 统计指标卡片 */
|
||||
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
|
||||
.stat-card{padding:16px;border-radius:8px;text-align:center;}
|
||||
.stat-1{background:#E8F3FF;}
|
||||
.stat-2{background:#FFECE8;}
|
||||
.stat-3{background:#E6FFEA;}
|
||||
.stat-4{background:#FFF7E6;}
|
||||
.stat-label{font-size:13px;color:#4E5969;}
|
||||
.stat-num{font-size:26px;font-weight:600;margin:8px 0;}
|
||||
|
||||
/* 图表区域 固定高度 防止溢出 */
|
||||
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px;}
|
||||
.chart-box{width:100%;height:260px;position:relative;}
|
||||
canvas{width:100% !important;height:100% !important;}
|
||||
|
||||
/* 表格 */
|
||||
.table-wrap{overflow-x:auto;}
|
||||
table{width:100%;border-collapse:collapse;font-size:14px;}
|
||||
th{background:#F5F7FA;padding:12px 10px;text-align:left;border-bottom:1px solid #E5E6EB;color:#4E5969;font-weight:500;white-space:nowrap;}
|
||||
td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
|
||||
.row-firing{background:#FFF7F7;}
|
||||
.row-resolved{background:#F7FFF9;}
|
||||
.storm{color:#F53F3F;font-weight:500;}
|
||||
|
||||
/* 分页 */
|
||||
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:16px;flex-wrap:wrap;gap:12px;}
|
||||
.pag-btn{height:32px;padding:0 12px;border:1px solid #DCDFE6;background:#fff;border-radius:6px;cursor:pointer;font-size:14px;}
|
||||
.pag-info{color:#4E5969;}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-mask{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);display:none;align-items:center;justify-content:center;z-index:999;}
|
||||
.modal-box{width:760px;max-height:80vh;background:#fff;border-radius:8px;padding:24px;overflow-y:auto;}
|
||||
.modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
|
||||
.modal-close{cursor:pointer;font-size:20px;color:#4E5969;}
|
||||
.labels-block{background:#F5F7FA;padding:12px;border-radius:6px;margin:12px 0;white-space:pre-wrap;font-family:monospace;font-size:13px;}
|
||||
|
||||
/* 空数据、提示文字 */
|
||||
.empty-box{padding:60px;text-align:center;color:#86909C;}
|
||||
.tip-text{font-size:13px;color:#86909C;margin-left:8px;}
|
||||
.shortcut-tip{text-align:center;color:#86909C;font-size:13px;margin-top:12px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header-bar">
|
||||
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
|
||||
<div class="user-info"><i class="fa fa-user"></i> <?php echo $userName; ?></div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<div class="card">
|
||||
<div class="filter-row filter-full">
|
||||
<input type="text" id="globalSearch" placeholder="<?php echo $t['global_search']; ?>" style="width:100%;">
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<div class="filter-item">
|
||||
<label><?php echo $t['start_date']; ?></label>
|
||||
<input type="date" id="startDate" value="<?php echo $today; ?>">
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label><?php echo $t['end_date']; ?></label>
|
||||
<input type="date" id="endDate" value="<?php echo $today; ?>">
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label><?php echo $t['filter_instance']; ?></label>
|
||||
<input type="text" id="filterInstance" placeholder="10.150.10.82">
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label><?php echo $t['severity_title']; ?></label>
|
||||
<div class="check-group">
|
||||
<label><input type="checkbox" name="sev" value="critical"> Critical</label>
|
||||
<label><input type="checkbox" name="sev" value="warning"> Warning</label>
|
||||
<label><input type="checkbox" name="sev" value="info"> Info</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label><?php echo $t['status_title']; ?></label>
|
||||
<div class="check-group">
|
||||
<label><input type="checkbox" name="sta" value="firing"> Firing</label>
|
||||
<label><input type="checkbox" name="sta" value="resolved"> Resolved</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<button class="btn btn-primary" id="btnSearch"><i class="fa fa-search"></i> <?php echo $t['btn_query']; ?></button>
|
||||
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> <?php echo $t['btn_reset']; ?></button>
|
||||
<button class="btn btn-warning" id="btnToggleDistinct"><i class="fa fa-list"></i> <?php echo $t['btn_detail_mode']; ?></button>
|
||||
<span class="tip-text" id="modeTip"><?php echo $t['tip_distinct']; ?></span>
|
||||
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> <?php echo $t['btn_export']; ?></button>
|
||||
<button class="btn btn-outline" id="btnCopyLink"><i class="fa fa-link"></i> <?php echo $t['btn_copy_link']; ?></button>
|
||||
<label style="display:flex;align-items:center;gap:6px;">
|
||||
<input type="checkbox" id="autoRefresh"> <?php echo $t['btn_refresh_auto']; ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 + 图表 -->
|
||||
<div class="card" id="statCard" style="display:none;">
|
||||
<div class="stat-row">
|
||||
<div class="stat-card stat-1">
|
||||
<div class="stat-label"><?php echo $t['stat_total']; ?></div>
|
||||
<div class="stat-num" id="statTotal">0</div>
|
||||
</div>
|
||||
<div class="stat-card stat-2">
|
||||
<div class="stat-label"><?php echo $t['stat_firing']; ?></div>
|
||||
<div class="stat-num" id="statFiring">0</div>
|
||||
</div>
|
||||
<div class="stat-card stat-3">
|
||||
<div class="stat-label"><?php echo $t['stat_resolved']; ?></div>
|
||||
<div class="stat-num" id="statResolved">0</div>
|
||||
</div>
|
||||
<div class="stat-card stat-4">
|
||||
<div class="stat-label"><?php echo $t['stat_critical']; ?></div>
|
||||
<div class="stat-num" id="statCritical">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-row">
|
||||
<div>
|
||||
<h4 class="card-title"><?php echo $t['hour_chart_title']; ?></h4>
|
||||
<div class="chart-box">
|
||||
<canvas id="hourChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="card-title"><?php echo $t['pie_title']; ?></h4>
|
||||
<div class="chart-box">
|
||||
<canvas id="pieChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 告警表格 -->
|
||||
<div class="card">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead id="tableHead"></thead>
|
||||
<tbody id="tableBody">
|
||||
<tr><td colspan="6" class="empty-box">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<div class="pag-info"><?php echo $t['stat_total']; ?>:<span id="pageTotal">0</span></div>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<span>每页</span>
|
||||
<select id="pageSizeSel" class="pag-btn">
|
||||
<option value="10">10</option>
|
||||
<option value="20" selected>20</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
<button class="pag-btn" id="pageFirst">首页</button>
|
||||
<button class="pag-btn" id="pagePrev">上一页</button>
|
||||
<span id="pageInfo">1 / 1</span>
|
||||
<button class="pag-btn" id="pageNext">下一页</button>
|
||||
<button class="pag-btn" id="pageLast">末页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-tip"><?php echo $t['shortcut_tip']; ?></div>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<div class="modal-mask" id="modalMask">
|
||||
<div class="modal-box">
|
||||
<div class="modal-head">
|
||||
<h3 id="modalTitle"><?php echo $t['modal_title']; ?></h3>
|
||||
<span class="modal-close" id="modalClose">×</span>
|
||||
</div>
|
||||
<div id="modalContent"></div>
|
||||
<div style="margin-top:16px;">
|
||||
<a target="_blank" id="grafanaLink" class="btn btn-primary"><i class="fa fa-chart-line"></i> <?php echo $t['modal_grafana']; ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const apiUrl = "./api/api_alert_list.php";
|
||||
const grafanaBase = "<?php echo $grafanaUrl; ?>";
|
||||
let page = 1;
|
||||
let pageSize = 20;
|
||||
let total = 0;
|
||||
let distinctMode = 1;
|
||||
let autoRefreshTimer = null;
|
||||
let hourChart = null;
|
||||
let pieChart = null;
|
||||
const stormThreshold = 1000;
|
||||
const t = <?php echo json_encode($t); ?>;
|
||||
|
||||
// 本地筛选记忆
|
||||
function saveFilter() {
|
||||
const data = {
|
||||
start: $("#startDate").val(),
|
||||
end: $("#endDate").val(),
|
||||
instance: $("#filterInstance").val(),
|
||||
global: $("#globalSearch").val(),
|
||||
sev: $("input[name=sev]:checked").map((i,e)=>e.value).get(),
|
||||
sta: $("input[name=sta]").map((i,e)=>e.value).get(),
|
||||
distinct: distinctMode,
|
||||
pageSize: pageSize
|
||||
};
|
||||
localStorage.setItem("alertFilter", JSON.stringify(data));
|
||||
}
|
||||
function loadFilter() {
|
||||
const s = localStorage.getItem("alertFilter");
|
||||
if (!s) return;
|
||||
const d = JSON.parse(s);
|
||||
$("#startDate").val(d.start);
|
||||
$("#endDate").val(d.end);
|
||||
$("#filterInstance").val(d.instance);
|
||||
$("#globalSearch").val(d.global);
|
||||
pageSize = d.pageSize;
|
||||
$("#pageSizeSel").val(pageSize);
|
||||
distinctMode = d.distinct;
|
||||
$("#modeTip").text(distinctMode === 1 ? t.tip_distinct : t.tip_raw);
|
||||
$("input[name=sev]").prop("checked", false);
|
||||
d.sev.forEach(v=>$(`input[name=sev][value="${v}"]`).prop("checked",true));
|
||||
$("input[name=sta]").prop("checked", false);
|
||||
d.sta.forEach(v=>$(`input[name=sta][value="${v}"]`).prop("checked",true));
|
||||
}
|
||||
|
||||
// 渲染表头
|
||||
function renderHead() {
|
||||
let h = "";
|
||||
if (distinctMode === 1) {
|
||||
h = `<tr>
|
||||
<th>${t['col_state']}</th>
|
||||
<th>${t['col_alert']}</th>
|
||||
<th>${t['col_instance']}</th>
|
||||
<th>${t['col_severity']}</th>
|
||||
<th>${t['col_count']}</th>
|
||||
<th>${t['col_time']}</th>
|
||||
<th>操作</th>
|
||||
</tr>`;
|
||||
} else {
|
||||
h = `<tr>
|
||||
<th>${t['col_state']}</th>
|
||||
<th>${t['col_alert']}</th>
|
||||
<th>${t['col_instance']}</th>
|
||||
<th>${t['col_severity']}</th>
|
||||
<th>${t['col_time']}</th>
|
||||
<th>操作</th>
|
||||
</tr>`;
|
||||
}
|
||||
$("#tableHead").html(h);
|
||||
}
|
||||
|
||||
// 绘图 严格限制尺寸、精简配置 解决撑满页面问题
|
||||
function drawChart(stat) {
|
||||
$("#statCard").show();
|
||||
$("#statTotal").text(stat.total_all);
|
||||
$("#statFiring").text(stat.total_firing);
|
||||
$("#statResolved").text(stat.total_resolved);
|
||||
$("#statCritical").text(stat.total_critical);
|
||||
const hourData = stat.hour_data;
|
||||
const hourLabels = [];
|
||||
const hourVals = [];
|
||||
for (let i=0;i<24;i++) {
|
||||
hourLabels.push(i+":00");
|
||||
hourVals.push(hourData[i]);
|
||||
}
|
||||
// 销毁旧图表防止叠加溢出
|
||||
if (hourChart) hourChart.destroy();
|
||||
const hCtx = document.getElementById("hourChart").getContext("2d");
|
||||
hourChart = new Chart(hCtx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: hourLabels,
|
||||
datasets: [{
|
||||
label: "触发量",
|
||||
data: hourVals,
|
||||
borderColor: "#165DFF",
|
||||
backgroundColor: "rgba(22, 93, 255, 0.08)",
|
||||
fill: true,
|
||||
tension: 0.2,
|
||||
pointRadius: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {display:true,position:"top",labels:{boxWidth:12,font:{size:12}}},
|
||||
tooltip: {mode:"index",intersect:false}
|
||||
},
|
||||
scales: {
|
||||
x: {grid:{display:false},ticks:{font:{size:11}}},
|
||||
y: {grid:{color:"#F2F3F5"},ticks:{font:{size:11}}}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (pieChart) pieChart.destroy();
|
||||
const pCtx = document.getElementById("pieChart").getContext("2d");
|
||||
let c=0,w=0,i=0;
|
||||
$("#tableBody tr").each((idx,tr)=>{
|
||||
const s = $(tr).find("td:nth-child(4)").text();
|
||||
if (s.includes("critical")) c++;
|
||||
else if (s.includes("warning")) w++;
|
||||
else i++;
|
||||
})
|
||||
pieChart = new Chart(pCtx, {
|
||||
type:"doughnut",
|
||||
data:{
|
||||
labels:["Critical","Warning","Info"],
|
||||
datasets:[{
|
||||
data:[c,w,i],
|
||||
backgroundColor:["#F53F3F","#FF7D00","#165DFF"],
|
||||
borderWidth:0
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
responsive:true,
|
||||
maintainAspectRatio:false,
|
||||
plugins:{legend:{position:"bottom",labels:{boxWidth:12,font:{size:12}}}},
|
||||
cutout:"60%" // 缩小饼图半径,不会超大
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
function loadData() {
|
||||
saveFilter();
|
||||
renderHead();
|
||||
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">Loading...</td></tr>`);
|
||||
const sevArr = $("input[name=sev]:checked").map((i,e)=>e.value).get();
|
||||
const staArr = $("input[name=sta]").map((i,e)=>e.value).get();
|
||||
const params = new URLSearchParams();
|
||||
params.append("page", page);
|
||||
params.append("page_size", pageSize);
|
||||
params.append("start_date", $("#startDate").val());
|
||||
params.append("end_date", $("#endDate").val());
|
||||
params.append("instance", $("#filterInstance").val());
|
||||
params.append("global", $("#globalSearch").val());
|
||||
params.append("distinct", distinctMode);
|
||||
sevArr.forEach(v=>params.append("severity[]",v));
|
||||
staArr.forEach(v=>params.append("status[]",v));
|
||||
fetch(`${apiUrl}?${params.toString()}`)
|
||||
.then(r=>r.json())
|
||||
.then(res=>{
|
||||
if (res.code !== 0) {
|
||||
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;"><i class="fa fa-exclamation-circle"></i> ${res.msg}</td></tr>`);
|
||||
$("#statCard").hide();
|
||||
return;
|
||||
}
|
||||
total = res.total;
|
||||
$("#pageTotal").text(total);
|
||||
const totalPage = Math.ceil(total / pageSize) || 1;
|
||||
$("#pageInfo").text(`${page} / ${totalPage}`);
|
||||
if(res.stat.total_all > 0){
|
||||
drawChart(res.stat);
|
||||
}else{
|
||||
$("#statCard").hide();
|
||||
}
|
||||
const list = res.list;
|
||||
if (list.length === 0) {
|
||||
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">${t.empty_tip}</td></tr>`);
|
||||
return;
|
||||
}
|
||||
let html = "";
|
||||
list.forEach(row=>{
|
||||
const cls = row.state === "firing" ? "row-firing" : "row-resolved";
|
||||
const tagClass = row.state === "firing" ? "tag-firing" : "tag-resolved";
|
||||
const tagText = row.state === "firing" ? "Firing" : "Resolved";
|
||||
let sevCls = "";
|
||||
if (row.severity === "critical") sevCls = "severity-critical";
|
||||
else if (row.severity === "warning") sevCls = "severity-warning";
|
||||
else sevCls = "severity-info";
|
||||
let storm = "";
|
||||
if (distinctMode === 1 && row.total_count > stormThreshold) storm = `<span class="storm">${t.storm_tip}</span>`;
|
||||
if (distinctMode === 1) {
|
||||
html += `<tr class="${cls}" data-row='${JSON.stringify(row)}'>
|
||||
<td><span class="${tagClass}">${tagText}</span></td>
|
||||
<td>${row.metric}</td>
|
||||
<td>${row.instance}</td>
|
||||
<td><span class="${sevCls}">${row.severity}</span></td>
|
||||
<td>${row.total_count} ${storm}</td>
|
||||
<td>${row.time}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">${t.btn_copy_name}</button>
|
||||
<button class="btn btn-sm btn-outline detail-open">详情</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
} else {
|
||||
html += `<tr class="${cls}" data-row='${JSON.stringify(row)}'>
|
||||
<td><span class="${tagClass}">${tagText}</span></td>
|
||||
<td>${row.metric}</td>
|
||||
<td>${row.instance}</td>
|
||||
<td><span class="${sevCls}">${row.severity}</span></td>
|
||||
<td>${row.time}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">${t.btn_copy_name}</button>
|
||||
<button class="btn btn-sm btn-outline detail-open">详情</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
})
|
||||
$("#tableBody").html(html);
|
||||
})
|
||||
.catch(()=>{
|
||||
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;"><i class="fa fa-exclamation-circle"></i> 接口请求失败</td></tr>`);
|
||||
$("#statCard").hide();
|
||||
})
|
||||
}
|
||||
|
||||
// 导出CSV
|
||||
function exportExcel() {
|
||||
const sevArr = $("input[name=sev]:checked").map((i,e)=>e.value).get();
|
||||
const staArr = $("input[name=sta]").map((i,e)=>e.value).get();
|
||||
const params = new URLSearchParams();
|
||||
params.append("export",1);
|
||||
params.append("page_size", 9999);
|
||||
params.append("start_date", $("#startDate").val());
|
||||
params.append("end_date", $("#endDate").val());
|
||||
params.append("instance", $("#filterInstance").val());
|
||||
params.append("global", $("#globalSearch").val());
|
||||
params.append("distinct", distinctMode);
|
||||
sevArr.forEach(v=>params.append("severity[]",v));
|
||||
staArr.forEach(v=>params.append("status[]",v));
|
||||
fetch(`${apiUrl}?${params.toString()}`)
|
||||
.then(r=>r.json())
|
||||
.then(res=>{
|
||||
if (res.code !== 0) return alert(res.msg);
|
||||
let csv = "";
|
||||
if (distinctMode === 1) {
|
||||
csv = "状态,告警名称,实例,级别,触发次数,最新时间\n";
|
||||
res.list.forEach(r=>csv += `${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${r.time}\n`);
|
||||
} else {
|
||||
csv = "状态,告警名称,实例,级别,触发时间\n";
|
||||
res.list.forEach(r=>csv += `${r.state},${r.metric},${r.instance},${r.severity},${r.time}\n`);
|
||||
}
|
||||
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = "alert_export_"+new Date().getTime()+".csv";
|
||||
a.click();
|
||||
})
|
||||
}
|
||||
|
||||
// 复制筛选链接
|
||||
function copyLink() {
|
||||
const sevArr = $("input[name=sev]:checked").map((i,e)=>e.value).get();
|
||||
const staArr = $("input[name=sta]").map((i,e)=>e.value).get();
|
||||
const params = new URLSearchParams();
|
||||
params.append("start_date", $("#startDate").val());
|
||||
params.append("end_date", $("#endDate").val());
|
||||
params.append("instance", $("#filterInstance").val());
|
||||
params.append("global", $("#globalSearch").val());
|
||||
params.append("distinct", distinctMode);
|
||||
sevArr.forEach(v=>params.append("severity[]",v));
|
||||
staArr.forEach(v=>params.append("status[]",v));
|
||||
const link = location.origin + location.pathname + "?" + params.toString();
|
||||
navigator.clipboard.writeText(link).then(()=>alert("筛选链接已复制"));
|
||||
}
|
||||
|
||||
// 弹窗详情
|
||||
function openModal(row) {
|
||||
const metric = row.metric;
|
||||
const inst = row.instance;
|
||||
const grafanaLink = `${grafanaBase}/d/alert-overview?var-alert=${encodeURIComponent(metric)}&var-instance=${encodeURIComponent(inst)}`;
|
||||
$("#grafanaLink").attr("href", grafanaLink);
|
||||
let html = `<h4>${metric}</h4>
|
||||
<p>实例:${inst}|级别:${row.severity}|状态:${row.state}</p>
|
||||
<div class="labels-block">${JSON.stringify(row.labels, null, 2)}</div>`;
|
||||
$("#modalContent").html(html);
|
||||
$("#modalMask").css("display","flex");
|
||||
}
|
||||
|
||||
// 自动刷新开关
|
||||
function toggleAutoRefresh() {
|
||||
if ($("#autoRefresh").is(":checked")) {
|
||||
autoRefreshTimer = setInterval(()=>loadData(), 60000);
|
||||
} else {
|
||||
clearInterval(autoRefreshTimer);
|
||||
}
|
||||
}
|
||||
|
||||
$(function(){
|
||||
loadFilter();
|
||||
loadData();
|
||||
// 切换汇总/详细模式
|
||||
$("#btnToggleDistinct").click(()=>{
|
||||
distinctMode = distinctMode === 1 ? 0 : 1;
|
||||
$("#modeTip").text(distinctMode === 1 ? t.tip_distinct : t.tip_raw);
|
||||
page = 1;
|
||||
loadData();
|
||||
})
|
||||
// 查询按钮
|
||||
$("#btnSearch").click(()=>{page=1;loadData();});
|
||||
// 重置筛选
|
||||
$("#btnReset").click(()=>{
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
$("#startDate").val(today);
|
||||
$("#endDate").val(today);
|
||||
$("#filterInstance").val("");
|
||||
$("#globalSearch").val("");
|
||||
$("input[name=sev]").prop("checked",false);
|
||||
$("input[name=sta]").prop("checked",false);
|
||||
distinctMode = 1;
|
||||
$("#modeTip").text(t.tip_distinct);
|
||||
page = 1;
|
||||
loadData();
|
||||
})
|
||||
// 导出、复制链接、自动刷新
|
||||
$("#btnExport").click(exportExcel);
|
||||
$("#btnCopyLink").click(copyLink);
|
||||
$("#autoRefresh").change(toggleAutoRefresh);
|
||||
// 每页条数切换
|
||||
$("#pageSizeSel").change(function(){
|
||||
pageSize = parseInt($(this).val());
|
||||
page = 1;
|
||||
loadData();
|
||||
})
|
||||
// 分页按钮
|
||||
$("#pageFirst").click(()=>{page=1;loadData();});
|
||||
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}});
|
||||
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}});
|
||||
$("#pageLast").click(()=>{page=Math.ceil(total/pageSize);loadData();});
|
||||
// 复制告警名、打开详情弹窗
|
||||
$(document).on("click",".copy-name",function(){
|
||||
const name = $(this).data("name");
|
||||
navigator.clipboard.writeText(name).then(()=>alert("已复制告警名称:"+name));
|
||||
})
|
||||
$(document).on("click",".detail-open",function(){
|
||||
const rowJson = $(this).closest("tr").attr("data-row");
|
||||
const row = JSON.parse(rowJson);
|
||||
openModal(row);
|
||||
})
|
||||
// 关闭弹窗
|
||||
$("#modalClose, #modalMask").click(function(e){
|
||||
if (e.target === this) $("#modalMask").hide();
|
||||
})
|
||||
// 全局快捷键
|
||||
$(document).keydown(function(e){
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
page = 1;
|
||||
loadData();
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
$("#modalMask").hide();
|
||||
}
|
||||
if (e.ctrlKey && e.key.toLowerCase() === "f") {
|
||||
e.preventDefault();
|
||||
$("#globalSearch").focus();
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user