471 lines
15 KiB
JavaScript
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");
|