1009 lines
33 KiB
JavaScript
1009 lines
33 KiB
JavaScript
const { createApp } = Vue;
|
|
|
|
const STORAGE_KEY = "hy-app-monitor-refresh-interval";
|
|
const UPDATE_OPERATION_STORAGE_KEY = "hy-app-monitor-service-update-operation";
|
|
|
|
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: "",
|
|
validating: false,
|
|
validationMessage: "",
|
|
saving: false,
|
|
saveMessage: "",
|
|
loadedContent: "",
|
|
editor: {
|
|
group: "",
|
|
dataId: "",
|
|
type: "",
|
|
appName: "",
|
|
description: "",
|
|
lastModifiedTime: "",
|
|
md5: "",
|
|
content: "",
|
|
},
|
|
},
|
|
goConfig: {
|
|
loading: false,
|
|
error: "",
|
|
validating: false,
|
|
validationMessage: "",
|
|
saving: false,
|
|
saveMessage: "",
|
|
loadedContent: "",
|
|
editorContent: "",
|
|
diffKeys: [],
|
|
hosts: [],
|
|
},
|
|
updateOps: {
|
|
loading: false,
|
|
error: "",
|
|
running: false,
|
|
operationId: "",
|
|
status: "",
|
|
stage: "",
|
|
startedAt: "",
|
|
finishedAt: "",
|
|
logDroppedCount: 0,
|
|
logs: [],
|
|
message: "",
|
|
items: [],
|
|
selectedServices: [],
|
|
javaGitRef: "main",
|
|
goGitRef: "main",
|
|
imageTag: "",
|
|
skipMavenBuild: false,
|
|
defaults: {
|
|
javaGitRef: "main",
|
|
goGitRef: "main",
|
|
dockerPlatform: "linux/amd64",
|
|
},
|
|
builder: {
|
|
harborRegistry: "",
|
|
harborProject: "",
|
|
javaRepoRoot: "",
|
|
golangRepoRoot: "",
|
|
},
|
|
result: null,
|
|
pollTimer: null,
|
|
statusRequestInFlight: false,
|
|
statusRequestSeq: 0,
|
|
statusAppliedSeq: 0,
|
|
requestEpoch: 0,
|
|
},
|
|
restartOps: {
|
|
loading: false,
|
|
error: "",
|
|
running: false,
|
|
message: "",
|
|
items: [],
|
|
selectedServices: [],
|
|
result: null,
|
|
},
|
|
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);
|
|
});
|
|
},
|
|
nacosConfigDirty() {
|
|
return this.nacosConfig.editor.content !== this.nacosConfig.loadedContent;
|
|
},
|
|
goConfigDirty() {
|
|
return this.goConfig.editorContent !== this.goConfig.loadedContent;
|
|
},
|
|
restartTargetMap() {
|
|
return Object.fromEntries((this.restartOps.items || []).map((item) => [item.service, item]));
|
|
},
|
|
updateTargetMap() {
|
|
return Object.fromEntries((this.updateOps.items || []).map((item) => [item.service, item]));
|
|
},
|
|
updateLogText() {
|
|
return (this.updateOps.logs || [])
|
|
.map((entry) => {
|
|
const time = this.formatDateTime(entry.time);
|
|
const stage = entry.stage ? ` [${entry.stage}]` : "";
|
|
const level = entry.level && entry.level !== "info" ? ` (${entry.level})` : "";
|
|
return `${time}${stage}${level} ${entry.message || ""}`.trim();
|
|
})
|
|
.join("\n");
|
|
},
|
|
},
|
|
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"}`;
|
|
},
|
|
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`;
|
|
},
|
|
restartTargetLabel(item) {
|
|
return `${item.service} · ${item.kind} · ${item.hosts.map((host) => host.host).join(" / ")}`;
|
|
},
|
|
updateTargetLabel(item) {
|
|
return `${item.service} · ${item.kind} · ${item.hosts.map((host) => host.host).join(" / ")}`;
|
|
},
|
|
updateLogDownloadUrl() {
|
|
if (!this.updateOps.operationId) {
|
|
return "";
|
|
}
|
|
const query = new URLSearchParams({ id: this.updateOps.operationId });
|
|
return `/api/ops/update-log?${query.toString()}`;
|
|
},
|
|
updateStatusBadgeClass() {
|
|
if (this.updateOps.error || this.updateOps.status === "failed") {
|
|
return "bad";
|
|
}
|
|
if (this.updateOps.running || this.updateOps.status === "running" || this.updateOps.status === "queued") {
|
|
return "warn";
|
|
}
|
|
if (this.updateOps.status === "success") {
|
|
return "ok";
|
|
}
|
|
return "neutral";
|
|
},
|
|
updateStatusLabel() {
|
|
if (this.updateOps.error || this.updateOps.status === "failed") {
|
|
return "失败";
|
|
}
|
|
if (this.updateOps.status === "queued") {
|
|
return "排队中";
|
|
}
|
|
if (this.updateOps.running || this.updateOps.status === "running") {
|
|
return "执行中";
|
|
}
|
|
if (this.updateOps.status === "success") {
|
|
return "已完成";
|
|
}
|
|
return "未开始";
|
|
},
|
|
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();
|
|
}
|
|
},
|
|
persistUpdateOperationId() {
|
|
if (this.updateOps.operationId) {
|
|
window.sessionStorage.setItem(UPDATE_OPERATION_STORAGE_KEY, this.updateOps.operationId);
|
|
return;
|
|
}
|
|
window.sessionStorage.removeItem(UPDATE_OPERATION_STORAGE_KEY);
|
|
},
|
|
stopUpdatePolling() {
|
|
if (this.updateOps.pollTimer) {
|
|
window.clearInterval(this.updateOps.pollTimer);
|
|
this.updateOps.pollTimer = null;
|
|
}
|
|
},
|
|
scrollUpdateLogToBottom() {
|
|
this.$nextTick(() => {
|
|
const node = document.getElementById("service-update-log-output");
|
|
if (node) {
|
|
node.scrollTop = node.scrollHeight;
|
|
}
|
|
});
|
|
},
|
|
applyUpdateOperation(operation) {
|
|
if (!operation) {
|
|
return;
|
|
}
|
|
|
|
this.updateOps.operationId = operation.operationId || "";
|
|
this.updateOps.status = operation.status || "";
|
|
this.updateOps.stage = operation.stage || "";
|
|
this.updateOps.startedAt = operation.startedAt || "";
|
|
this.updateOps.finishedAt = operation.finishedAt || "";
|
|
this.updateOps.logDroppedCount = Number(operation.logDroppedCount || 0);
|
|
this.updateOps.logs = Array.isArray(operation.logs) ? operation.logs : [];
|
|
this.updateOps.result = operation.result || null;
|
|
this.updateOps.running = operation.status === "queued" || operation.status === "running";
|
|
|
|
if (operation.status === "success") {
|
|
this.updateOps.message = operation.message || operation.result?.message || "服务更新完成";
|
|
this.updateOps.error = "";
|
|
} else if (operation.status === "failed") {
|
|
this.updateOps.error = operation.error || operation.result?.error || "服务更新失败";
|
|
this.updateOps.message = operation.message || "";
|
|
} else {
|
|
this.updateOps.message = operation.message || "";
|
|
}
|
|
|
|
this.persistUpdateOperationId();
|
|
this.scrollUpdateLogToBottom();
|
|
},
|
|
startUpdatePolling() {
|
|
this.stopUpdatePolling();
|
|
if (!this.updateOps.operationId) {
|
|
return;
|
|
}
|
|
this.updateOps.pollTimer = window.setInterval(() => {
|
|
this.fetchUpdateOperationStatus(this.updateOps.operationId, { silent: true });
|
|
}, 2000);
|
|
},
|
|
async fetchUpdateOperationStatus(operationId, { silent = false } = {}) {
|
|
if (!operationId) {
|
|
return;
|
|
}
|
|
|
|
// 轮询只保留一个在途请求,避免旧响应晚到后把新日志覆盖掉。
|
|
if (this.updateOps.statusRequestInFlight) {
|
|
return;
|
|
}
|
|
|
|
const wasRunning = this.updateOps.running;
|
|
const requestSeq = this.updateOps.statusRequestSeq + 1;
|
|
const requestEpoch = this.updateOps.requestEpoch;
|
|
this.updateOps.statusRequestSeq = requestSeq;
|
|
this.updateOps.statusRequestInFlight = true;
|
|
|
|
try {
|
|
const query = new URLSearchParams({ id: operationId });
|
|
const response = await fetch(`/api/ops/update-status?${query.toString()}`, { cache: "no-store" });
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
// 新任务已经开始时,直接丢掉旧任务轮询结果。
|
|
if (requestEpoch !== this.updateOps.requestEpoch) {
|
|
return;
|
|
}
|
|
|
|
if (this.updateOps.operationId && this.updateOps.operationId !== operationId) {
|
|
return;
|
|
}
|
|
|
|
// 保护日志顺序,旧轮询结果不能回退页面状态。
|
|
if (requestSeq < this.updateOps.statusAppliedSeq) {
|
|
return;
|
|
}
|
|
this.updateOps.statusAppliedSeq = requestSeq;
|
|
|
|
this.applyUpdateOperation(payload.operation || {});
|
|
|
|
if (!this.updateOps.running) {
|
|
this.stopUpdatePolling();
|
|
if (wasRunning) {
|
|
await this.refresh();
|
|
await this.refreshUpdateTargets();
|
|
await this.refreshRestartTargets();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (requestEpoch !== this.updateOps.requestEpoch) {
|
|
return;
|
|
}
|
|
|
|
if (this.updateOps.operationId && this.updateOps.operationId !== operationId) {
|
|
return;
|
|
}
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
const notFound = errorMessage.includes("update operation not found") || errorMessage.includes("HTTP 404");
|
|
|
|
if (silent && !notFound) {
|
|
return;
|
|
}
|
|
|
|
this.stopUpdatePolling();
|
|
this.updateOps.running = false;
|
|
|
|
if (notFound) {
|
|
this.updateOps.status = "failed";
|
|
this.updateOps.error = "更新任务状态不存在,可能因为服务重启或旧任务已清理";
|
|
this.updateOps.message = "";
|
|
this.updateOps.operationId = "";
|
|
this.persistUpdateOperationId();
|
|
return;
|
|
}
|
|
|
|
if (this.updateOps.status === "queued" || this.updateOps.status === "running") {
|
|
this.updateOps.status = "failed";
|
|
this.updateOps.message = "更新状态查询失败";
|
|
}
|
|
this.updateOps.error = errorMessage;
|
|
} finally {
|
|
if (requestEpoch === this.updateOps.requestEpoch && this.updateOps.statusRequestSeq === requestSeq) {
|
|
this.updateOps.statusRequestInFlight = false;
|
|
}
|
|
}
|
|
},
|
|
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.validationMessage = "";
|
|
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 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;
|
|
}
|
|
},
|
|
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.validationMessage = "";
|
|
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 validateNacosConfig() {
|
|
if (!this.nacosConfig.editor.dataId) {
|
|
return;
|
|
}
|
|
|
|
this.nacosConfig.validating = true;
|
|
this.nacosConfig.detailError = "";
|
|
this.nacosConfig.validationMessage = "";
|
|
|
|
try {
|
|
const response = await fetch("/api/nacos/config/validate", {
|
|
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}`);
|
|
}
|
|
if (!payload.ok) {
|
|
throw new Error(payload.message || "config validation failed");
|
|
}
|
|
this.nacosConfig.validationMessage = payload.message || "格式校验通过";
|
|
} catch (error) {
|
|
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
|
|
} finally {
|
|
this.nacosConfig.validating = 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 refreshGoConfig() {
|
|
this.goConfig.loading = true;
|
|
this.goConfig.error = "";
|
|
this.goConfig.saveMessage = "";
|
|
|
|
try {
|
|
const response = await fetch("/api/runtime/golang-config", { cache: "no-store" });
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
this.goConfig.hosts = Array.isArray(payload.hosts) ? payload.hosts : [];
|
|
this.goConfig.diffKeys = Array.isArray(payload.diffKeys) ? payload.diffKeys : [];
|
|
this.goConfig.loadedContent = typeof payload.editorContent === "string" ? payload.editorContent : "";
|
|
this.goConfig.editorContent = this.goConfig.loadedContent;
|
|
} catch (error) {
|
|
this.goConfig.error = error instanceof Error ? error.message : String(error);
|
|
} finally {
|
|
this.goConfig.loading = false;
|
|
}
|
|
},
|
|
async validateGoConfig() {
|
|
this.goConfig.validating = true;
|
|
this.goConfig.error = "";
|
|
this.goConfig.validationMessage = "";
|
|
|
|
try {
|
|
const response = await fetch("/api/runtime/golang-config/validate", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ content: this.goConfig.editorContent }),
|
|
});
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
if (!payload.ok) {
|
|
throw new Error(payload.message || "golang config validation failed");
|
|
}
|
|
this.goConfig.validationMessage = payload.message || "格式校验通过";
|
|
} catch (error) {
|
|
this.goConfig.error = error instanceof Error ? error.message : String(error);
|
|
} finally {
|
|
this.goConfig.validating = false;
|
|
}
|
|
},
|
|
async saveGoConfig() {
|
|
this.goConfig.saving = true;
|
|
this.goConfig.error = "";
|
|
this.goConfig.saveMessage = "";
|
|
|
|
try {
|
|
const response = await fetch("/api/runtime/golang-config", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ content: this.goConfig.editorContent }),
|
|
});
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
this.goConfig.loadedContent = this.goConfig.editorContent;
|
|
this.goConfig.saveMessage = payload.message || "golang 配置已保存";
|
|
await this.refreshGoConfig();
|
|
} catch (error) {
|
|
this.goConfig.error = error instanceof Error ? error.message : String(error);
|
|
} finally {
|
|
this.goConfig.saving = false;
|
|
}
|
|
},
|
|
async refreshRestartTargets() {
|
|
this.restartOps.loading = true;
|
|
this.restartOps.error = "";
|
|
|
|
try {
|
|
const response = await fetch("/api/ops/restart-targets", { cache: "no-store" });
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
this.restartOps.items = Array.isArray(payload.items) ? payload.items : [];
|
|
const available = new Set(this.restartOps.items.map((item) => item.service));
|
|
this.restartOps.selectedServices = this.restartOps.selectedServices.filter((service) => available.has(service));
|
|
} catch (error) {
|
|
this.restartOps.error = error instanceof Error ? error.message : String(error);
|
|
} finally {
|
|
this.restartOps.loading = false;
|
|
}
|
|
},
|
|
async refreshUpdateTargets() {
|
|
this.updateOps.loading = true;
|
|
this.updateOps.error = "";
|
|
|
|
try {
|
|
const response = await fetch("/api/ops/update-targets", { cache: "no-store" });
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
this.updateOps.items = Array.isArray(payload.items) ? payload.items : [];
|
|
this.updateOps.defaults = payload.defaults || this.updateOps.defaults;
|
|
this.updateOps.builder = payload.builder || this.updateOps.builder;
|
|
|
|
if (!this.updateOps.javaGitRef) {
|
|
this.updateOps.javaGitRef = this.updateOps.defaults.javaGitRef || "main";
|
|
} else if (this.updateOps.javaGitRef === "main" && this.updateOps.defaults.javaGitRef) {
|
|
this.updateOps.javaGitRef = this.updateOps.defaults.javaGitRef;
|
|
}
|
|
|
|
if (!this.updateOps.goGitRef) {
|
|
this.updateOps.goGitRef = this.updateOps.defaults.goGitRef || "main";
|
|
} else if (this.updateOps.goGitRef === "main" && this.updateOps.defaults.goGitRef) {
|
|
this.updateOps.goGitRef = this.updateOps.defaults.goGitRef;
|
|
}
|
|
|
|
const available = new Set(this.updateOps.items.map((item) => item.service));
|
|
this.updateOps.selectedServices = this.updateOps.selectedServices.filter((service) => available.has(service));
|
|
} catch (error) {
|
|
this.updateOps.error = error instanceof Error ? error.message : String(error);
|
|
} finally {
|
|
this.updateOps.loading = false;
|
|
}
|
|
},
|
|
toggleUpdateService(serviceName) {
|
|
const next = new Set(this.updateOps.selectedServices);
|
|
if (next.has(serviceName)) {
|
|
next.delete(serviceName);
|
|
} else {
|
|
next.add(serviceName);
|
|
}
|
|
this.updateOps.selectedServices = [...next];
|
|
},
|
|
async runServiceUpdate() {
|
|
if (this.updateOps.selectedServices.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.stopUpdatePolling();
|
|
this.updateOps.requestEpoch += 1;
|
|
this.updateOps.statusRequestInFlight = false;
|
|
this.updateOps.statusAppliedSeq = 0;
|
|
this.updateOps.running = true;
|
|
this.updateOps.error = "";
|
|
this.updateOps.message = "已提交更新任务,等待开始执行";
|
|
this.updateOps.operationId = "";
|
|
this.updateOps.status = "queued";
|
|
this.updateOps.stage = "queued";
|
|
this.updateOps.startedAt = "";
|
|
this.updateOps.finishedAt = "";
|
|
this.updateOps.logDroppedCount = 0;
|
|
this.updateOps.logs = [];
|
|
this.updateOps.result = null;
|
|
this.persistUpdateOperationId();
|
|
|
|
try {
|
|
const response = await fetch("/api/ops/update-services", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
services: this.updateOps.selectedServices,
|
|
javaGitRef: this.updateOps.javaGitRef,
|
|
goGitRef: this.updateOps.goGitRef,
|
|
imageTag: this.updateOps.imageTag,
|
|
skipMavenBuild: this.updateOps.skipMavenBuild,
|
|
}),
|
|
});
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
this.applyUpdateOperation(payload.operation || {});
|
|
this.startUpdatePolling();
|
|
await this.fetchUpdateOperationStatus(this.updateOps.operationId, { silent: true });
|
|
} catch (error) {
|
|
this.updateOps.error = error instanceof Error ? error.message : String(error);
|
|
this.updateOps.running = false;
|
|
} finally {
|
|
this.scrollUpdateLogToBottom();
|
|
}
|
|
},
|
|
toggleRestartService(serviceName) {
|
|
const next = new Set(this.restartOps.selectedServices);
|
|
if (next.has(serviceName)) {
|
|
next.delete(serviceName);
|
|
} else {
|
|
next.add(serviceName);
|
|
}
|
|
this.restartOps.selectedServices = [...next];
|
|
},
|
|
async runRollingRestart() {
|
|
if (this.restartOps.selectedServices.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.restartOps.running = true;
|
|
this.restartOps.error = "";
|
|
this.restartOps.message = "";
|
|
|
|
try {
|
|
const response = await fetch("/api/ops/rolling-restart", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ services: this.restartOps.selectedServices }),
|
|
});
|
|
const payload = await this.readJsonResponse(response);
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
this.restartOps.result = payload;
|
|
this.restartOps.message = payload.ok
|
|
? (payload.message || "滚动重启完成")
|
|
: (payload.error || "滚动重启失败");
|
|
|
|
await this.refresh();
|
|
await this.refreshRestartTargets();
|
|
} catch (error) {
|
|
this.restartOps.error = error instanceof Error ? error.message : String(error);
|
|
} finally {
|
|
this.restartOps.running = 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 });
|
|
this.refreshGoConfig();
|
|
this.refreshUpdateTargets();
|
|
this.refreshRestartTargets();
|
|
const lastOperationId = window.sessionStorage.getItem(UPDATE_OPERATION_STORAGE_KEY) || "";
|
|
if (lastOperationId) {
|
|
this.fetchUpdateOperationStatus(lastOperationId, { silent: true });
|
|
this.startUpdatePolling();
|
|
}
|
|
},
|
|
beforeUnmount() {
|
|
if (this.timer) {
|
|
window.clearInterval(this.timer);
|
|
}
|
|
this.stopUpdatePolling();
|
|
},
|
|
}).mount("#app");
|