283 lines
8.2 KiB
JavaScript

const { createApp } = Vue;
const STORAGE_KEY = "hy-app-monitor-refresh-interval";
createApp({
data() {
return {
loading: false,
error: "",
filter: "all",
keyword: "",
refreshIntervalSeconds: 10,
refreshIntervalOptions: [5, 10, 15, 30, 60],
payload: {
updatedAt: "",
settings: {
refreshIntervalSeconds: 10,
refreshIntervalOptions: [5, 10, 15, 30, 60],
thresholds: {},
},
summary: {
total: 0,
ok: 0,
down: 0,
hostTotal: 0,
hostMetricOk: 0,
hostMetricDown: 0,
nacosServiceTotal: 0,
nacosInstanceTotal: 0,
nacosUnhealthyInstanceTotal: 0,
mongoDatabaseTotal: 0,
mongoCollectionTotal: 0,
mongoOk: 0,
},
hosts: [],
nacos: {
ok: false,
summary: {},
services: [],
namespaces: [],
},
mongo: {
ok: false,
summary: {},
databases: [],
replicaSet: {
configured: false,
members: [],
},
},
},
timer: null,
};
},
computed: {
summary() {
return this.payload.summary || {};
},
thresholds() {
return this.payload.settings?.thresholds || {};
},
formattedUpdatedAt() {
return this.formatDateTime(this.payload.updatedAt);
},
groupedHosts() {
const keyword = this.keyword.trim().toLowerCase();
return (this.payload.hosts || [])
.map((host) => {
const hostMatched = [host.host, host.ip, host.group, host.instanceId]
.join(" ")
.toLowerCase()
.includes(keyword);
const filteredItems = (host.items || []).filter((item) => {
if (this.filter === "down" && item.ok) {
return false;
}
if (this.filter === "java" && item.kind !== "java") {
return false;
}
if (this.filter === "golang" && item.kind !== "golang") {
return false;
}
if (!keyword) {
return true;
}
return [item.host, item.ip, item.service, item.kind, item.group, item.url, item.detail]
.join(" ")
.toLowerCase()
.includes(keyword);
});
const shouldShowBecauseDown = this.filter === "down" && !(host.metrics && host.metrics.ok);
const shouldShowBecauseKeyword = keyword ? hostMatched || filteredItems.length > 0 : true;
const shouldShow =
shouldShowBecauseKeyword &&
(this.filter === "all" || filteredItems.length > 0 || shouldShowBecauseDown || hostMatched);
return {
...host,
groupLabel: this.groupLabel(host.group),
items: filteredItems,
downCount: (host.items || []).filter((item) => !item.ok).length,
shouldShow,
};
})
.filter((host) => host.shouldShow);
},
filteredNacosServices() {
const keyword = this.keyword.trim().toLowerCase();
return (this.payload.nacos?.services || []).filter((service) => {
if (!keyword) {
return true;
}
const serviceMatched = [service.serviceName, service.groupName, service.rawServiceName]
.join(" ")
.toLowerCase()
.includes(keyword);
if (serviceMatched) {
return true;
}
return (service.instances || []).some((instance) =>
[instance.ip, instance.port, instance.clusterName]
.join(" ")
.toLowerCase()
.includes(keyword)
);
});
},
},
watch: {
refreshIntervalSeconds() {
window.localStorage.setItem(STORAGE_KEY, String(this.refreshIntervalSeconds));
this.applyRefreshTimer();
},
},
methods: {
groupLabel(group) {
return {
app: "应用节点",
pay: "支付节点",
gateway: "网关节点",
nacos: "Nacos 节点",
mongo: "MongoDB 节点",
}[group] || group;
},
toneClass(level) {
return `tone-${level || "neutral"}`;
},
boolTone(ok) {
return ok ? "tone-ok" : "tone-bad";
},
formatPercent(value) {
if (value == null || Number.isNaN(Number(value))) {
return "-";
}
return `${Number(value).toFixed(2)}%`;
},
formatGb(used, total) {
if (used == null || total == null) {
return "-";
}
return `${Number(used).toFixed(2)} / ${Number(total).toFixed(2)} GB`;
},
formatMb(value) {
if (value == null || Number.isNaN(Number(value))) {
return "-";
}
return `${Number(value).toFixed(2)} MB`;
},
formatBytes(value) {
if (value == null || Number.isNaN(Number(value))) {
return "-";
}
const units = ["B", "KB", "MB", "GB", "TB"];
let amount = Number(value);
let index = 0;
while (amount >= 1024 && index < units.length - 1) {
amount /= 1024;
index += 1;
}
return `${amount.toFixed(2)} ${units[index]}`;
},
formatNumber(value) {
if (value == null || Number.isNaN(Number(value))) {
return "-";
}
return Number(value).toLocaleString();
},
formatDateTime(value) {
if (!value) {
return "-";
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
},
formatDuration(seconds) {
if (seconds == null || Number.isNaN(Number(seconds))) {
return "-";
}
const total = Math.max(0, Number(seconds));
const days = Math.floor(total / 86400);
const hours = Math.floor((total % 86400) / 3600);
const minutes = Math.floor((total % 3600) / 60);
const remainSeconds = Math.floor(total % 60);
if (days > 0) {
return `${days}d ${hours}h ${minutes}m`;
}
if (hours > 0) {
return `${hours}h ${minutes}m ${remainSeconds}s`;
}
if (minutes > 0) {
return `${minutes}m ${remainSeconds}s`;
}
return `${remainSeconds}s`;
},
syncRefreshSettings() {
const settings = this.payload.settings || {};
const options = Array.isArray(settings.refreshIntervalOptions)
? settings.refreshIntervalOptions.map((item) => Number(item)).filter((item) => !Number.isNaN(item) && item > 0)
: this.refreshIntervalOptions;
if (options.length > 0) {
this.refreshIntervalOptions = options;
}
const stored = Number(window.localStorage.getItem(STORAGE_KEY) || 0);
const configured = Number(settings.refreshIntervalSeconds || 0);
const fallback = this.refreshIntervalOptions[0] || 10;
const candidate = this.refreshIntervalOptions.includes(stored)
? stored
: this.refreshIntervalOptions.includes(configured)
? configured
: fallback;
if (candidate !== this.refreshIntervalSeconds) {
this.refreshIntervalSeconds = candidate;
} else {
this.applyRefreshTimer();
}
},
applyRefreshTimer() {
if (this.timer) {
window.clearInterval(this.timer);
}
this.timer = window.setInterval(() => this.refresh(), this.refreshIntervalSeconds * 1000);
},
async refresh() {
this.loading = true;
this.error = "";
try {
const response = await fetch("/api/monitor/services", { cache: "no-store" });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
this.payload = await response.json();
this.syncRefreshSettings();
} catch (error) {
this.error = error instanceof Error ? error.message : String(error);
} finally {
this.loading = false;
}
},
},
mounted() {
const root = document.getElementById("app");
const bootIndicator = document.getElementById("boot-indicator");
if (root) {
root.removeAttribute("v-cloak");
}
if (bootIndicator) {
bootIndicator.hidden = true;
}
this.refresh();
},
beforeUnmount() {
if (this.timer) {
window.clearInterval(this.timer);
}
},
}).mount("#app");