2026-04-22 14:39:40 +08:00

471 lines
15 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: [],
},
},
},
nacosConfig: {
loading: false,
error: "",
keyword: "",
items: [],
selectedKey: "",
detailLoading: false,
detailError: "",
saving: false,
saveMessage: "",
loadedContent: "",
editor: {
group: "",
dataId: "",
type: "",
appName: "",
description: "",
lastModifiedTime: "",
md5: "",
content: "",
},
},
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)
);
});
},
filteredNacosConfigs() {
const keyword = this.nacosConfig.keyword.trim().toLowerCase();
return (this.nacosConfig.items || []).filter((item) => {
if (!keyword) {
return true;
}
return [item.group, item.dataId, item.type, item.appName, item.description]
.join(" ")
.toLowerCase()
.includes(keyword);
});
},
selectedNacosConfig() {
return (this.nacosConfig.items || []).find((item) => this.configKey(item.group, item.dataId) === this.nacosConfig.selectedKey) || null;
},
nacosConfigDirty() {
return this.nacosConfig.editor.content !== this.nacosConfig.loadedContent;
},
},
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";
},
configKey(group, dataId) {
return `${group}@@${dataId}`;
},
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);
},
resetNacosEditor() {
this.nacosConfig.selectedKey = "";
this.nacosConfig.detailError = "";
this.nacosConfig.saveMessage = "";
this.nacosConfig.loadedContent = "";
this.nacosConfig.editor = {
group: "",
dataId: "",
type: "",
appName: "",
description: "",
lastModifiedTime: "",
md5: "",
content: "",
};
},
async readJsonResponse(response) {
const text = await response.text();
if (!text) {
return {};
}
try {
return JSON.parse(text);
} catch (error) {
throw new Error(text);
}
},
async refreshNacosConfigs({ preserveSelection = true, reloadSelected = false } = {}) {
const previousKey = preserveSelection ? this.nacosConfig.selectedKey : "";
this.nacosConfig.loading = true;
this.nacosConfig.error = "";
this.nacosConfig.saveMessage = "";
try {
const response = await fetch("/api/nacos/configs", { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.nacosConfig.items = Array.isArray(payload.items) ? payload.items : [];
const hasPrevious = previousKey
&& this.nacosConfig.items.some((item) => this.configKey(item.group, item.dataId) === previousKey);
const nextKey = hasPrevious
? previousKey
: (this.nacosConfig.items[0]
? this.configKey(this.nacosConfig.items[0].group, this.nacosConfig.items[0].dataId)
: "");
if (!nextKey) {
this.resetNacosEditor();
return;
}
this.nacosConfig.selectedKey = nextKey;
const sameAsCurrent = this.configKey(this.nacosConfig.editor.group, this.nacosConfig.editor.dataId) === nextKey;
if (!sameAsCurrent || reloadSelected || !this.nacosConfig.loadedContent) {
await this.openNacosConfigByKey(nextKey);
}
} catch (error) {
this.nacosConfig.error = error instanceof Error ? error.message : String(error);
} finally {
this.nacosConfig.loading = false;
}
},
async openNacosConfig(item) {
const nextKey = this.configKey(item.group, item.dataId);
this.nacosConfig.selectedKey = nextKey;
await this.openNacosConfigByKey(nextKey);
},
async openNacosConfigByKey(configKey) {
const item = (this.nacosConfig.items || []).find((entry) => this.configKey(entry.group, entry.dataId) === configKey);
if (!item) {
return;
}
this.nacosConfig.detailLoading = true;
this.nacosConfig.detailError = "";
this.nacosConfig.saveMessage = "";
try {
const query = new URLSearchParams({ group: item.group, dataId: item.dataId });
const response = await fetch(`/api/nacos/config?${query.toString()}`, { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.nacosConfig.loadedContent = typeof payload.content === "string" ? payload.content : "";
this.nacosConfig.editor = {
group: item.group,
dataId: item.dataId,
type: item.type || payload.item?.type || "",
appName: item.appName || "",
description: item.description || "",
lastModifiedTime: item.lastModifiedTime || "",
md5: item.md5 || "",
content: this.nacosConfig.loadedContent,
};
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
} finally {
this.nacosConfig.detailLoading = false;
}
},
async saveNacosConfig() {
if (!this.nacosConfig.editor.group || !this.nacosConfig.editor.dataId) {
return;
}
this.nacosConfig.saving = true;
this.nacosConfig.detailError = "";
this.nacosConfig.saveMessage = "";
try {
const response = await fetch("/api/nacos/config", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
group: this.nacosConfig.editor.group,
dataId: this.nacosConfig.editor.dataId,
type: this.nacosConfig.editor.type,
content: this.nacosConfig.editor.content,
}),
});
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.nacosConfig.loadedContent = this.nacosConfig.editor.content;
this.nacosConfig.saveMessage = `已保存 ${this.nacosConfig.editor.group}/${this.nacosConfig.editor.dataId}`;
await this.refreshNacosConfigs({ preserveSelection: true, reloadSelected: true });
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
} finally {
this.nacosConfig.saving = false;
}
},
async refresh() {
this.loading = true;
this.error = "";
try {
const response = await fetch("/api/monitor/services", { cache: "no-store" });
const payload = await this.readJsonResponse(response);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.payload = payload;
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();
this.refreshNacosConfigs({ preserveSelection: false, reloadSelected: true });
},
beforeUnmount() {
if (this.timer) {
window.clearInterval(this.timer);
}
},
}).mount("#app");