2026-04-27 18:42:56 +08:00

6518 lines
222 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { createApp } = Vue;
const APP_TEMPLATE = document.getElementById("app")?.innerHTML || "";
const APP_CONTEXTS = [
{ id: "yumi", basePath: "/yumi", mode: "suite", title: "HY App Monitor" },
{ id: "common", basePath: "/common", mode: "common", title: "HY App Monitor 通用" },
{ id: "mysql", basePath: "/mysql", mode: "common", title: "HY App Monitor 通用" },
];
const APP_SWITCH_LINKS = [
{ id: "yumi", label: "Yumi", href: "/yumi/" },
{ id: "haiyi", label: "HaiYi", href: "/haiyi/" },
{ id: "common", label: "通用", href: "/common/" },
];
const STORAGE_KEY = "hy-app-monitor-refresh-interval";
const UPDATE_OPERATION_STORAGE_KEY = "hy-app-monitor-service-update-operation";
const GROUP_LABELS = {
app: "应用节点",
pay: "支付节点",
gateway: "网关节点",
nacos: "Nacos 节点",
mongo: "MongoDB 节点",
};
const GROUP_ORDER = {
gateway: 10,
app: 20,
pay: 30,
nacos: 40,
mongo: 50,
};
const VIEW_TABS = [
{ id: "overview", label: "总览" },
{ id: "services", label: "服务" },
{ id: "hosts", label: "主机" },
{ id: "infra", label: "基础设施" },
{ id: "database", label: "数据库" },
{ id: "config", label: "配置" },
{ id: "release", label: "发布" },
];
const DATABASE_ENGINES = [
{ id: "mysql", label: "MySQL" },
{ id: "mongo", label: "MongoDB" },
];
const COMMON_MODULE_VIEW_TABS = [
{ id: "common", label: "通用" },
];
const COMMON_TOOL_TABS = [
{ id: "mysql", label: "MySQL" },
{ id: "cdn", label: "CDN 刷新" },
{ id: "cron", label: "Cron 部署" },
];
const PRIMARY_FILTERS = [
{ id: "all", label: "全部" },
{ id: "down", label: "异常" },
{ id: "gateway", label: "网关" },
{ id: "app", label: "应用" },
{ id: "pay", label: "支付" },
{ id: "infra", label: "基础设施" },
];
const BUSINESS_GROUPS = new Set(["gateway", "app", "pay"]);
const CONFIG_TABS = [
{ id: "nacos", label: "Nacos" },
{ id: "go", label: "Go Env" },
{ id: "history", label: "历史版本" },
];
const INFRA_TABS = [
{ id: "overview", label: "状态总览" },
{ id: "nacos", label: "Nacos 服务" },
{ id: "mongo", label: "Mongo 指标" },
{ id: "tat", label: "TAT" },
];
const MONGO_ADMIN_TABS = [
{ id: "browse", label: "浏览" },
{ id: "write", label: "写入" },
{ id: "aggregate", label: "聚合" },
{ id: "indexes", label: "索引" },
];
const MYSQL_ADMIN_TABS = [
{ id: "browse", label: "数据" },
{ id: "write", label: "写入" },
{ id: "schema", label: "结构" },
{ id: "sql", label: "SQL" },
];
const MYSQL_FILTER_OPERATORS = [
{ id: "eq", label: "=" },
{ id: "ne", label: "!=" },
{ id: "gt", label: ">" },
{ id: "ge", label: ">=" },
{ id: "lt", label: "<" },
{ id: "le", label: "<=" },
{ id: "in", label: "IN" },
{ id: "not_in", label: "NOT IN" },
{ id: "between", label: "BETWEEN" },
{ id: "not_between", label: "NOT BETWEEN" },
{ id: "like", label: "LIKE" },
{ id: "not_like", label: "NOT LIKE" },
{ id: "is_null", label: "IS NULL" },
{ id: "is_not_null", label: "IS NOT NULL" },
];
const MYSQL_INDEX_MODES = [
{ id: "normal", label: "普通" },
{ id: "unique", label: "唯一" },
{ id: "fulltext", label: "全文" },
];
const MYSQL_DEFAULT_MODES = [
{ id: "none", label: "无默认值" },
{ id: "literal", label: "字面量" },
{ id: "null", label: "NULL" },
];
function detectAppContext() {
const pathname = window.location.pathname || "/";
return APP_CONTEXTS.find((item) => (
pathname === item.basePath || pathname.startsWith(`${item.basePath}/`)
)) || { id: "root", basePath: "", mode: "suite", title: "HY App Monitor" };
}
const CURRENT_APP_CONTEXT = detectAppContext();
const APP_BASE_PATH = CURRENT_APP_CONTEXT.basePath;
document.title = CURRENT_APP_CONTEXT.title;
function rootPath(path) {
const normalized = typeof path === "string" && path.startsWith("/") ? path : `/${path || ""}`;
return normalized;
}
function appPath(path) {
const normalized = typeof path === "string" && path.startsWith("/") ? path : `/${path || ""}`;
return `${APP_BASE_PATH}${normalized}` || "/";
}
function loginRedirectPath() {
return rootPath(`/login?next=${encodeURIComponent(`${window.location.pathname}${window.location.search}`)}`);
}
function createMongoAdminState() {
return {
loaded: false,
loading: false,
detailLoading: false,
querying: false,
error: "",
message: "",
messageTone: "neutral",
keyword: "",
activeTab: "browse",
pageSizeOptions: [50, 100, 200],
tabs: MONGO_ADMIN_TABS,
databases: [],
selectedDatabase: "",
selectedCollection: "",
collectionDetail: {
updatedAt: "",
stats: {
count: 0,
sizeMb: 0,
storageSizeMb: 0,
avgObjSizeBytes: 0,
indexCount: 0,
totalIndexSizeMb: 0,
capped: false,
wiredTigerUri: "",
},
indexes: [],
},
query: {
filterText: "{}",
sortText: JSON.stringify({ _id: -1 }, null, 2),
projectionText: "{}",
page: 1,
pageSize: 100,
total: 0,
pageCount: 1,
items: [],
},
editor: {
selectedKey: "",
idText: "",
filterText: "{}",
documentText: "{}",
upsert: false,
saving: false,
},
createDocument: {
documentText: "{}",
ordered: true,
saving: false,
},
updateOp: {
filterText: "{}",
updateText: JSON.stringify({ $set: {} }, null, 2),
many: false,
upsert: false,
running: false,
},
deleteOp: {
filterText: "{}",
many: false,
running: false,
},
aggregate: {
pipelineText: "[]",
page: 1,
pageSize: 100,
total: 0,
pageCount: 1,
items: [],
running: false,
},
distinct: {
field: "",
filterText: "{}",
values: [],
valuesText: "[]",
running: false,
count: 0,
},
indexForm: {
keysText: JSON.stringify({ field: 1 }, null, 2),
optionsText: "{}",
saving: false,
dropAllSaving: false,
},
collectionOps: {
createDatabaseName: "",
createName: "",
createOptionsText: "{}",
renameName: "",
renameDropTarget: false,
running: false,
dropRunning: false,
databaseDropRunning: false,
},
};
}
function createMySqlOverviewState() {
return {
ok: false,
updatedAt: "",
info: {},
connections: {},
throughput: {},
replication: {
configured: false,
role: "",
readOnly: false,
superReadOnly: false,
sourceHost: "",
secondsBehindSource: null,
ioRunning: false,
sqlRunning: false,
state: "",
error: "",
},
summary: {
databaseTotal: 0,
tableTotal: 0,
dataSizeMb: 0,
indexSizeMb: 0,
totalSizeMb: 0,
},
databases: [],
warnings: [],
error: "",
};
}
function createMySqlFilterRow() {
return {
column: "",
operator: "eq",
value: "",
};
}
function createMySqlAdminState() {
return {
loaded: false,
loading: false,
tablesLoading: false,
schemaLoading: false,
rowsLoading: false,
error: "",
message: "",
messageTone: "neutral",
keyword: "",
activeTab: "browse",
pageSizeOptions: [50, 100, 200],
tabs: MYSQL_ADMIN_TABS,
filterOperators: MYSQL_FILTER_OPERATORS,
indexModes: MYSQL_INDEX_MODES,
defaultModes: MYSQL_DEFAULT_MODES,
overview: createMySqlOverviewState(),
databases: [],
tables: [],
selectedDatabase: "",
selectedTable: "",
schema: {
updatedAt: "",
tableSummary: {},
columns: [],
indexes: [],
identity: {
mode: "all",
columns: [],
},
createTable: "",
},
rows: {
page: 1,
pageSize: 100,
totalRows: 0,
totalPages: 1,
keyword: "",
orderBy: "",
orderDirection: "desc",
filters: [createMySqlFilterRow()],
columns: [],
indexes: [],
identity: {
mode: "all",
columns: [],
},
items: [],
},
rowEditor: {
identityKey: "",
identity: null,
valuesText: "{}",
saving: false,
},
insertForm: {
valuesText: "{}",
saving: false,
},
columnForm: {
columnName: "",
columnType: "varchar(255)",
nullable: true,
defaultMode: "none",
defaultValue: "",
comment: "",
position: "last",
afterColumn: "",
saving: false,
},
indexForm: {
indexName: "",
indexMode: "normal",
columns: [],
saving: false,
},
sqlConsole: {
text: "",
running: false,
result: null,
},
};
}
function createCdnAdminState() {
return {
loaded: false,
loading: false,
submitting: false,
error: "",
message: "",
messageTone: "neutral",
keyword: "",
overview: {
ok: false,
configured: false,
updatedAt: "",
error: "",
warnings: [],
settings: {
allowedDomains: [],
taskLimit: 20,
},
quota: {
url: [],
path: [],
},
tasks: [],
taskTotal: 0,
},
form: {
purgeType: "url",
area: "",
flushType: "flush",
urlEncode: false,
itemsText: "",
},
lastResult: null,
};
}
function createChatappCronDeployState() {
return {
loaded: false,
loading: false,
deploying: false,
error: "",
message: "",
messageTone: "neutral",
form: {
gitRef: "main",
},
overview: {
ok: false,
enabled: false,
configured: false,
updatedAt: "",
settings: {
enabled: false,
serviceName: "chatapp-cron",
host: "",
instanceId: "",
sshHost: "",
repoRoot: "",
repoUrl: "",
defaultGitRef: "main",
remoteDir: "",
composeFile: "",
composeService: "week-star-job",
binaryPath: "",
dockerPlatform: "linux/amd64",
},
metadata: {},
},
operationId: "",
status: "",
stage: "",
startedAt: "",
finishedAt: "",
logs: [],
result: null,
pollTimer: null,
statusRequestInFlight: false,
};
}
createApp({
template: APP_TEMPLATE,
data() {
const isCommonMode = CURRENT_APP_CONTEXT.mode === "common";
return {
activeView: isCommonMode ? "common" : "overview",
appSwitchLinks: APP_SWITCH_LINKS,
currentAppEntryId: CURRENT_APP_CONTEXT.id === "mysql" ? "common" : CURRENT_APP_CONTEXT.id,
databaseEngine: isCommonMode ? "mysql" : "mongo",
commonTool: "mysql",
densityMode: "compact",
detailPanelOpen: false,
overviewHealthyHostsOpen: false,
viewTabs: isCommonMode ? COMMON_MODULE_VIEW_TABS : VIEW_TABS,
databaseEngines: isCommonMode
? DATABASE_ENGINES.filter((engine) => engine.id === "mysql")
: DATABASE_ENGINES.filter((engine) => engine.id === "mongo"),
commonToolTabs: COMMON_TOOL_TABS,
primaryFilters: PRIMARY_FILTERS,
configTabs: CONFIG_TABS,
infraTabs: INFRA_TABS,
mongoAdminTabs: MONGO_ADMIN_TABS,
mySqlAdminTabs: MYSQL_ADMIN_TABS,
expandedHosts: [],
expandedNacosServices: [],
collapsedMongoDatabases: [],
focusedHostKey: "",
selectedServiceKey: "",
loading: false,
error: "",
filter: "all",
keyword: "",
infraKeyword: "",
releaseKeyword: "",
buildParamsOpen: false,
configTab: "nacos",
configEditorMode: "diff",
goEditorMode: "diff",
infraTab: "overview",
infraHealthyNacosOpen: false,
infraHealthyTatOpen: false,
infraMongoDetailsOpen: false,
collapsedConfigGroups: [],
copiedToken: "",
copiedTokenTimer: null,
refreshIntervalSeconds: 10,
refreshIntervalOptions: [5, 10, 15, 30, 60],
auth: {
username: "",
checking: false,
redirecting: false,
loggingOut: false,
},
configHistory: [],
releaseHistory: [],
releasePrecheck: {
ok: false,
ranAt: "",
tone: "neutral",
title: "",
checks: [],
},
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,
mysqlDatabaseTotal: 0,
mysqlTableTotal: 0,
mysqlOk: 0,
},
hosts: [],
nacos: {
ok: false,
error: "",
baseUrl: "",
namespaceName: "",
namespaceId: "",
summary: {},
services: [],
namespaces: [],
},
mongo: {
ok: false,
error: "",
info: {},
summary: {},
connections: {},
memory: {},
network: {},
opcounters: {},
wiredTigerCache: {},
databases: [],
replicaSet: {
configured: false,
members: [],
},
},
mysql: createMySqlOverviewState(),
},
nacosConfig: {
loading: false,
error: "",
keyword: "",
items: [],
selectedKey: "",
detailLoading: false,
detailError: "",
validating: false,
validationMessage: "",
saving: false,
saveMessage: "",
baseUrl: "",
namespaceId: "",
namespaceName: "",
loadedContent: "",
editor: {
group: "",
dataId: "",
type: "",
appName: "",
description: "",
lastModifiedTime: "",
md5: "",
content: "",
},
},
goConfig: {
loading: false,
error: "",
validating: false,
validationMessage: "",
saving: false,
saveMessage: "",
loadedContent: "",
editorContent: "",
diffKeys: [],
hosts: [],
},
mongoAdmin: createMongoAdminState(),
mySqlAdmin: createMySqlAdminState(),
cdnAdmin: createCdnAdminState(),
chatappCron: createChatappCronDeployState(),
updateOps: {
loading: false,
loaded: false,
error: "",
running: false,
operationId: "",
status: "",
stage: "",
startedAt: "",
finishedAt: "",
logDroppedCount: 0,
logs: [],
message: "",
items: [],
selectedServices: [],
javaGitRef: "main",
goGitRef: "main",
adminGitRef: "main",
imageTag: "",
skipMavenBuild: false,
defaults: {
javaGitRef: "main",
goGitRef: "main",
adminGitRef: "main",
dockerPlatform: "linux/amd64",
},
builder: {
harborRegistry: "",
harborProject: "",
javaRepoRoot: "",
golangRepoRoot: "",
frontendRepoRoot: "",
frontendRepoUrl: "",
frontendPublicUrl: "",
},
result: null,
pollTimer: null,
statusRequestInFlight: false,
statusRequestSeq: 0,
statusAppliedSeq: 0,
requestEpoch: 0,
},
restartOps: {
loading: false,
loaded: false,
error: "",
running: false,
message: "",
items: [],
selectedServices: [],
result: null,
},
serviceLog: {
loading: false,
error: "",
service: "",
host: "",
container: "",
content: "",
updatedAt: "",
previewLines: 1000,
exportLines: 1000,
exportMaxLines: 10000,
exporting: false,
},
logViewer: {
open: false,
kind: "",
},
timer: null,
};
},
computed: {
summary() {
return this.payload.summary || {};
},
thresholds() {
return this.payload.settings?.thresholds || {};
},
formattedUpdatedAt() {
if (CURRENT_APP_CONTEXT.mode === "common") {
return this.formatDateTime(
this.chatappCron.overview.updatedAt || this.cdnAdmin.overview.updatedAt || this.mySqlAdmin.overview.updatedAt || "",
);
}
return this.formatDateTime(this.payload.updatedAt);
},
globalHealthTone() {
if (this.error) {
return "bad";
}
if (this.loading && !this.hasRenderableData) {
return "warn";
}
if (CURRENT_APP_CONTEXT.mode === "common") {
if (this.commonTool === "mysql" && this.mySqlAdmin.error) {
return "bad";
}
if (this.commonTool === "cdn" && this.cdnAdmin.error) {
return "bad";
}
if (this.commonTool === "cron" && this.chatappCron.error) {
return "bad";
}
return "neutral";
}
const nacosDown = !(this.payload.nacos && this.payload.nacos.ok);
const mongoDown = !(this.payload.mongo && this.payload.mongo.ok);
const serviceDown = Number(this.summary.down || 0) > 0;
const hostMetricDown = Number(this.summary.hostMetricDown || 0) > 0;
const nacosUnhealthy = Number(this.summary.nacosUnhealthyInstanceTotal || 0) > 0;
if (serviceDown || nacosDown || mongoDown) {
return "bad";
}
if (hostMetricDown || nacosUnhealthy) {
return "warn";
}
return "ok";
},
globalHealthLabel() {
if (this.error) {
return "全局异常";
}
if (this.loading && !this.hasRenderableData) {
return "加载中";
}
if (CURRENT_APP_CONTEXT.mode === "common") {
return this.globalHealthTone === "bad" ? "通用异常" : "通用在线";
}
if (this.globalHealthTone === "bad") {
return `全局异常 ${this.summary.down || 0}`;
}
if (this.globalHealthTone === "warn") {
return "需要关注";
}
return "全局正常";
},
globalSearchModel: {
get() {
if (["overview", "services", "hosts"].includes(this.activeView)) {
return this.keyword;
}
if (this.activeView === "infra") {
return this.infraKeyword;
}
if (this.activeView === "database") {
return this.databaseKeywordModel;
}
if (this.activeView === "config") {
return this.configTab === "go" ? "" : this.nacosConfig.keyword;
}
if (this.activeView === "release") {
return this.releaseKeyword;
}
if (this.activeView === "common") {
if (this.commonTool === "mysql") {
return this.mySqlAdmin.keyword;
}
if (this.commonTool === "cdn") {
return this.cdnAdmin.keyword;
}
return "";
}
return "";
},
set(value) {
const nextValue = String(value || "");
if (["overview", "services", "hosts"].includes(this.activeView)) {
this.keyword = nextValue;
return;
}
if (this.activeView === "infra") {
this.infraKeyword = nextValue;
return;
}
if (this.activeView === "database") {
this.databaseKeywordModel = nextValue;
return;
}
if (this.activeView === "config") {
if (this.configTab !== "go") {
this.nacosConfig.keyword = nextValue;
}
return;
}
if (this.activeView === "release") {
this.releaseKeyword = nextValue;
return;
}
if (this.activeView === "common") {
if (this.commonTool === "mysql") {
this.mySqlAdmin.keyword = nextValue;
} else if (this.commonTool === "cdn") {
this.cdnAdmin.keyword = nextValue;
}
}
},
},
globalSearchPlaceholder() {
if (["overview", "services", "hosts"].includes(this.activeView)) {
return "搜索主机 / 服务 / IP";
}
if (this.activeView === "infra") {
return this.infraTab === "mongo" ? "搜索数据库 / 指标" : this.infraTab === "tat" ? "搜索主机 / IP / 节点" : "搜索服务 / namespace / IP";
}
if (this.activeView === "database") {
return this.databaseSearchPlaceholder;
}
if (this.activeView === "config") {
return this.configTab === "history" ? "搜索操作 / 结果 / 时间" : this.configTab === "go" ? "Go Env 当前无搜索" : "搜索 dataId / group / appName";
}
if (this.activeView === "release") {
return "搜索服务 / 主机 / 镜像";
}
if (this.activeView === "common") {
if (this.commonTool === "mysql") {
return "搜索数据库 / 表";
}
if (this.commonTool === "cdn") {
return "搜索 taskId / URL / 状态";
}
return "chatapp-cron 部署无搜索";
}
return "全局搜索";
},
globalSearchDisabled() {
return (this.activeView === "config" && this.configTab === "go")
|| (this.activeView === "common" && this.commonTool === "cron");
},
dataFreshnessLabel() {
const updatedAt = Date.parse(this.payload.updatedAt || "");
if (Number.isNaN(updatedAt)) {
return "未知";
}
const seconds = Math.max(0, Math.round((Date.now() - updatedAt) / 1000));
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes}m`;
}
return `${Math.floor(minutes / 60)}h`;
},
dataFreshnessTone() {
const updatedAt = Date.parse(this.payload.updatedAt || "");
if (Number.isNaN(updatedAt)) {
return "warn";
}
const seconds = Math.max(0, Math.round((Date.now() - updatedAt) / 1000));
if (seconds > 180) {
return "warn";
}
return "neutral";
},
hasRenderableData() {
return (
Boolean(this.payload.updatedAt)
|| this.allServiceRows.length > 0
|| this.allHostRows.length > 0
|| (this.payload.nacos?.services || []).length > 0
|| (this.payload.mongo?.databases || []).length > 0
|| (this.nacosConfig.items || []).length > 0
|| (this.goConfig.hosts || []).length > 0
|| (this.updateOps.items || []).length > 0
|| (this.restartOps.items || []).length > 0
|| this.mySqlAdmin.loaded
|| this.cdnAdmin.loaded
|| this.chatappCron.loaded
);
},
showLoadingSkeleton() {
return this.loading && !this.hasRenderableData && !this.error;
},
normalizedKeyword() {
return this.keyword.trim().toLowerCase();
},
normalizedInfraKeyword() {
return this.infraKeyword.trim().toLowerCase();
},
normalizedReleaseKeyword() {
return this.releaseKeyword.trim().toLowerCase();
},
allHostRows() {
return this.buildHostRows({ businessOnly: false, applyFilter: false, applyKeyword: false });
},
hostServiceAlertTotal() {
return this.allHostRows.filter((host) => host.downCount > 0).length;
},
businessHosts() {
return this.buildHostRows({ businessOnly: true, applyFilter: false, applyKeyword: false });
},
overviewHostRows() {
return this.buildHostRows({ businessOnly: true, applyFilter: true, applyKeyword: true });
},
overviewAlertHosts() {
return this.overviewHostRows.filter((host) => host.hasAlert);
},
overviewQuietHosts() {
return this.overviewHostRows.filter((host) => !host.hasAlert);
},
filteredHostRows() {
return this.buildHostRows({ businessOnly: false, applyFilter: true, applyKeyword: true });
},
filteredHostAlertTotal() {
return this.filteredHostRows.filter((host) => host.hasAlert).length;
},
filteredHostMetricIssueTotal() {
return this.filteredHostRows.filter((host) => host.hasMetricIssue).length;
},
overviewHostRowGroups() {
return this.groupHosts(this.overviewHostRows);
},
hostRowGroups() {
return this.groupHosts(this.filteredHostRows);
},
detailPanelVisible() {
return this.detailPanelOpen;
},
serviceMatrixColumns() {
if (this.filter === "infra") {
return [];
}
let columns = this.businessHosts;
if (["gateway", "app", "pay"].includes(this.filter)) {
columns = columns.filter((host) => host.group === this.filter);
}
if (this.focusedHostKey) {
columns = columns.filter((host) => host.host === this.focusedHostKey);
}
if (!this.normalizedKeyword) {
return columns;
}
const keywordMatched = columns.filter((host) => host.searchText.includes(this.normalizedKeyword));
return keywordMatched.length > 0 ? keywordMatched : columns;
},
serviceMatrixColumnGroups() {
const groups = [];
this.serviceMatrixColumns.forEach((host) => {
const last = groups[groups.length - 1];
if (last && last.group === host.group) {
last.span += 1;
return;
}
groups.push({
group: host.group,
label: this.groupLabel(host.group),
span: 1,
});
});
return groups;
},
allServiceRows() {
return this.buildServiceRows({
applyFilter: false,
applyKeyword: false,
columns: this.businessHosts,
});
},
serviceMatrixRows() {
return this.buildServiceRows({
applyFilter: true,
applyKeyword: true,
columns: this.serviceMatrixColumns,
});
},
serviceMatrixSections() {
const groups = new Map();
this.serviceMatrixRows.forEach((row) => {
if (!groups.has(row.group)) {
groups.set(row.group, {
group: row.group,
label: this.groupLabel(row.group),
rows: [],
columns: this.serviceMatrixColumns.filter((host) => host.group === row.group),
});
}
groups.get(row.group).rows.push(row);
});
return [...groups.values()]
.filter((section) => section.rows.length > 0 && section.columns.length > 0)
.map((section) => ({
...section,
rows: section.rows.map((row) => ({
...row,
sectionCells: section.columns.map((host) => row.instanceMap[host.host] || {
host: host.host,
ip: host.ip,
group: host.group,
groupLabel: host.groupLabel,
present: false,
ok: false,
level: "neutral",
statusCode: null,
latencyMs: null,
port: null,
detail: "",
url: "",
}),
})),
}))
.sort((left, right) => this.groupRank(left.group) - this.groupRank(right.group));
},
selectedServiceDetail() {
return this.serviceMatrixRows.find((row) => row.key === this.selectedServiceKey) || this.serviceMatrixRows[0] || null;
},
selectedServiceHosts() {
return new Set((this.selectedServiceDetail?.instances || []).map((instance) => instance.host));
},
nacosConfigGroups() {
const groups = new Map();
this.filteredNacosConfigs.forEach((item) => {
const groupKey = item.group || "default";
if (!groups.has(groupKey)) {
groups.set(groupKey, {
key: groupKey,
label: groupKey,
items: [],
});
}
groups.get(groupKey).items.push(item);
});
return [...groups.values()].map((group) => ({
...group,
items: group.items.sort((left, right) => left.dataId.localeCompare(right.dataId, "zh-CN")),
}));
},
selectedNacosConfigGroup() {
return this.nacosConfigGroups.find((group) =>
group.items.some((item) => this.configKey(item.group, item.dataId) === this.nacosConfig.selectedKey)
)?.key || "";
},
flatNacosConfigKeys() {
return this.nacosConfigGroups.flatMap((group) =>
group.items.map((item) => this.configKey(item.group, item.dataId))
);
},
selectedNacosConfigIndex() {
return this.flatNacosConfigKeys.indexOf(this.nacosConfig.selectedKey);
},
selectedPrevNacosConfigKey() {
return this.selectedNacosConfigIndex > 0
? this.flatNacosConfigKeys[this.selectedNacosConfigIndex - 1]
: "";
},
selectedNextNacosConfigKey() {
return this.selectedNacosConfigIndex >= 0 && this.selectedNacosConfigIndex < this.flatNacosConfigKeys.length - 1
? this.flatNacosConfigKeys[this.selectedNacosConfigIndex + 1]
: "";
},
configHistoryRows() {
const keyword = this.nacosConfig.keyword.trim().toLowerCase();
return this.configHistory.filter((item) => (
!keyword
|| [item.scope, item.title, item.detail, item.time]
.join(" ")
.toLowerCase()
.includes(keyword)
));
},
currentNacosDiffRows() {
return this.buildLineDiffRows(this.nacosConfig.loadedContent, this.nacosConfig.editor.content);
},
currentGoDiffRows() {
return this.buildLineDiffRows(this.goConfig.loadedContent, this.goConfig.editorContent);
},
showNacosDiffEmpty() {
return this.configEditorMode === "diff" && this.currentNacosDiffRows.length === 0;
},
showGoDiffEmpty() {
return this.goEditorMode === "diff" && this.currentGoDiffRows.length === 0;
},
filteredNacosServices() {
const keyword = this.normalizedInfraKeyword;
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)
);
});
},
filteredNacosServiceRows() {
return this.filteredNacosServices.sort((left, right) => (
Number((right.unhealthyTotal || 0) > 0) - Number((left.unhealthyTotal || 0) > 0)
|| (right.unhealthyTotal || 0) - (left.unhealthyTotal || 0)
|| left.serviceName.localeCompare(right.serviceName, "zh-CN")
));
},
infraAlertNacosServices() {
return this.filteredNacosServiceRows.filter((service) => (service.unhealthyTotal || 0) > 0);
},
infraHealthyNacosServices() {
return this.filteredNacosServiceRows.filter((service) => (service.unhealthyTotal || 0) === 0);
},
infraVisibleHealthyNacosServices() {
return this.infraHealthyNacosOpen ? this.infraHealthyNacosServices : [];
},
filteredMongoDatabases() {
const keyword = this.normalizedInfraKeyword;
return (this.payload.mongo.databases || []).filter((db) => (
!keyword
|| [db.name, db.collections, db.objects, db.dataSizeMb, db.storageSizeMb]
.join(" ")
.toLowerCase()
.includes(keyword)
));
},
mongoAdminKeyword() {
return this.mongoAdmin.keyword.trim().toLowerCase();
},
mongoAdminFilteredDatabases() {
return (this.mongoAdmin.databases || [])
.map((database) => {
const collections = Array.isArray(database.collections) ? database.collections : [];
if (!this.mongoAdminKeyword) {
return {
...database,
visibleCollections: collections,
};
}
const databaseMatched = database.name.toLowerCase().includes(this.mongoAdminKeyword);
const visibleCollections = databaseMatched
? collections
: collections.filter((collection) => (
[collection.name, collection.type, collection.documentCount, collection.indexCount]
.join(" ")
.toLowerCase()
.includes(this.mongoAdminKeyword)
));
if (!databaseMatched && visibleCollections.length === 0) {
return null;
}
return {
...database,
visibleCollections,
};
})
.filter(Boolean);
},
mongoAdminTotalCollectionCount() {
return (this.mongoAdmin.databases || []).reduce(
(total, database) => total + ((database.collections || []).length || 0),
0,
);
},
selectedMongoDatabaseItem() {
return (this.mongoAdmin.databases || []).find((database) => database.name === this.mongoAdmin.selectedDatabase) || null;
},
selectedMongoCollectionItem() {
return (this.selectedMongoDatabaseItem?.collections || []).find(
(collection) => collection.name === this.mongoAdmin.selectedCollection,
) || null;
},
activeMongoAdminTabLabel() {
return this.mongoAdminTabs.find((item) => item.id === this.mongoAdmin.activeTab)?.label || "";
},
mongoAdminCollectionStats() {
return this.mongoAdmin.collectionDetail?.stats || {
count: 0,
sizeMb: 0,
storageSizeMb: 0,
avgObjSizeBytes: 0,
indexCount: 0,
totalIndexSizeMb: 0,
capped: false,
wiredTigerUri: "",
};
},
mongoAdminOverviewCards() {
const selectedDatabase = this.selectedMongoDatabaseItem;
const selectedCollection = this.selectedMongoCollectionItem;
const stats = this.mongoAdminCollectionStats;
const totalDatabaseCount = this.mongoAdmin.databases.length;
const totalCollectionCount = this.mongoAdminTotalCollectionCount;
const selectedDatabaseDocumentTotal = (selectedDatabase?.collections || []).reduce(
(total, collection) => total + Number(collection.documentCount || 0),
0,
);
const selectedDatabaseIndexTotal = (selectedDatabase?.collections || []).reduce(
(total, collection) => total + Number(collection.indexCount || 0),
0,
);
return [
{
key: "database",
label: "数据库",
value: selectedDatabase ? this.truncateMiddle(selectedDatabase.name, 14, 8) : `${this.formatNumber(totalDatabaseCount)}`,
meta: `${this.formatNumber(totalDatabaseCount)}`,
tone: this.mongoAdmin.error ? "bad" : "neutral",
},
{
key: "collection",
label: "集合",
value: selectedCollection ? this.truncateMiddle(selectedCollection.name, 16, 8) : `${this.formatNumber(selectedDatabase?.collectionCount || totalCollectionCount)}`,
meta: selectedCollection ? (selectedCollection.type || "collection") : (selectedDatabase ? selectedDatabase.name : "全部集合"),
tone: selectedCollection?.error ? "bad" : "neutral",
},
{
key: "documents",
label: "文档",
value: selectedCollection ? this.formatNumber(stats.count) : this.formatNumber(selectedDatabaseDocumentTotal),
meta: selectedCollection ? `平均 ${this.formatBytes(stats.avgObjSizeBytes || 0)}` : "当前库",
tone: this.mongoAdmin.error ? "bad" : "neutral",
},
{
key: "storage",
label: "容量",
value: selectedCollection ? this.formatMb(stats.storageSizeMb || 0) : this.formatMb(selectedDatabase?.sizeOnDiskMb || 0),
meta: selectedCollection ? `data ${this.formatMb(stats.sizeMb || 0)}` : "磁盘占用",
tone: this.mongoAdmin.error ? "bad" : "neutral",
},
{
key: "indexes",
label: "索引",
value: selectedCollection ? this.formatNumber(stats.indexCount) : this.formatNumber(selectedDatabaseIndexTotal),
meta: selectedCollection ? `size ${this.formatMb(stats.totalIndexSizeMb || 0)}` : `${this.formatNumber(totalCollectionCount)} 集合`,
tone: this.mongoAdmin.error ? "bad" : "neutral",
},
];
},
mongoQueryRangeText() {
if (!this.mongoAdmin.query.total || this.mongoAdmin.query.items.length === 0) {
return "0 / 0";
}
const start = (this.mongoAdmin.query.page - 1) * this.mongoAdmin.query.pageSize + 1;
const end = start + this.mongoAdmin.query.items.length - 1;
return `${start}-${end} / ${this.mongoAdmin.query.total}`;
},
mongoAggregateRangeText() {
if (!this.mongoAdmin.aggregate.total || this.mongoAdmin.aggregate.items.length === 0) {
return "0 / 0";
}
const start = (this.mongoAdmin.aggregate.page - 1) * this.mongoAdmin.aggregate.pageSize + 1;
const end = start + this.mongoAdmin.aggregate.items.length - 1;
return `${start}-${end} / ${this.mongoAdmin.aggregate.total}`;
},
mySqlAdminKeyword() {
return this.mySqlAdmin.keyword.trim().toLowerCase();
},
mySqlFilteredDatabases() {
return (this.mySqlAdmin.databases || []).filter((database) => {
if (!this.mySqlAdminKeyword) {
return true;
}
if (
[
database.name,
database.tableTotal,
database.rowEstimate,
database.totalSizeMb,
]
.join(" ")
.toLowerCase()
.includes(this.mySqlAdminKeyword)
) {
return true;
}
if (database.name !== this.mySqlAdmin.selectedDatabase) {
return false;
}
return (this.mySqlAdmin.tables || []).some((table) => (
[
table.name,
table.engine,
table.rowEstimate,
table.totalSizeMb,
]
.join(" ")
.toLowerCase()
.includes(this.mySqlAdminKeyword)
));
});
},
selectedMySqlDatabaseItem() {
return (this.mySqlAdmin.databases || []).find((database) => database.name === this.mySqlAdmin.selectedDatabase) || null;
},
mySqlVisibleTables() {
const tables = this.mySqlAdmin.tables || [];
if (!this.mySqlAdminKeyword) {
return tables;
}
const selectedMatched = this.selectedMySqlDatabaseItem
&& [
this.selectedMySqlDatabaseItem.name,
this.selectedMySqlDatabaseItem.tableTotal,
this.selectedMySqlDatabaseItem.rowEstimate,
this.selectedMySqlDatabaseItem.totalSizeMb,
]
.join(" ")
.toLowerCase()
.includes(this.mySqlAdminKeyword);
if (selectedMatched) {
return tables;
}
return tables.filter((table) => (
[
table.name,
table.engine,
table.rowEstimate,
table.totalSizeMb,
table.collation,
]
.join(" ")
.toLowerCase()
.includes(this.mySqlAdminKeyword)
));
},
selectedMySqlTableItem() {
return (this.mySqlAdmin.tables || []).find((table) => table.name === this.mySqlAdmin.selectedTable) || null;
},
activeMySqlAdminTabLabel() {
return this.mySqlAdminTabs.find((item) => item.id === this.mySqlAdmin.activeTab)?.label || "";
},
showDatabaseWorkbench() {
return this.activeView === "database" || (this.activeView === "common" && this.commonTool === "mysql");
},
showMySqlWorkbench() {
return (this.activeView === "database" && this.databaseEngine === "mysql")
|| (this.activeView === "common" && this.commonTool === "mysql");
},
activeDatabaseTabs() {
return this.databaseEngine === "mongo" ? this.mongoAdminTabs : this.mySqlAdminTabs;
},
activeDatabaseTabId: {
get() {
return this.databaseEngine === "mongo" ? this.mongoAdmin.activeTab : this.mySqlAdmin.activeTab;
},
set(value) {
if (this.databaseEngine === "mongo") {
this.mongoAdmin.activeTab = value;
} else {
this.mySqlAdmin.activeTab = value;
}
},
},
activeDatabaseTabLabel() {
return this.databaseEngine === "mongo" ? this.activeMongoAdminTabLabel : this.activeMySqlAdminTabLabel;
},
databaseKeywordModel: {
get() {
return this.databaseEngine === "mongo" ? this.mongoAdmin.keyword : this.mySqlAdmin.keyword;
},
set(value) {
if (this.databaseEngine === "mongo") {
this.mongoAdmin.keyword = value;
} else {
this.mySqlAdmin.keyword = value;
}
},
},
databaseContextLabel() {
return this.databaseEngine === "mongo"
? "MongoDB / 浏览 / 写入 / 聚合 / 索引"
: "MySQL / 数据 / 写入 / 结构 / SQL";
},
databaseSearchPlaceholder() {
return this.databaseEngine === "mongo" ? "搜索数据库 / 集合" : "搜索数据库 / 表";
},
databaseTreeLoading() {
return this.databaseEngine === "mongo" ? this.mongoAdmin.loading : this.mySqlAdmin.loading;
},
databaseRefreshBusy() {
if (this.databaseEngine === "mongo") {
return this.mongoAdmin.loading
|| this.mongoAdmin.detailLoading
|| this.mongoAdmin.querying
|| this.mongoAdmin.aggregate.running
|| this.mongoAdmin.distinct.running
|| this.mongoAdmin.editor.saving
|| this.mongoAdmin.createDocument.saving
|| this.mongoAdmin.updateOp.running
|| this.mongoAdmin.deleteOp.running
|| this.mongoAdmin.indexForm.saving
|| this.mongoAdmin.indexForm.dropAllSaving
|| this.mongoAdmin.collectionOps.running
|| this.mongoAdmin.collectionOps.dropRunning
|| this.mongoAdmin.collectionOps.databaseDropRunning;
}
return this.mySqlAdmin.loading
|| this.mySqlAdmin.tablesLoading
|| this.mySqlAdmin.schemaLoading
|| this.mySqlAdmin.rowsLoading
|| this.mySqlAdmin.sqlConsole.running
|| this.mySqlAdmin.rowEditor.saving
|| this.mySqlAdmin.insertForm.saving
|| this.mySqlAdmin.columnForm.saving
|| this.mySqlAdmin.indexForm.saving;
},
databaseHasSelection() {
return this.databaseEngine === "mongo" ? Boolean(this.mongoAdmin.selectedDatabase) : Boolean(this.mySqlAdmin.selectedDatabase);
},
databaseRefreshLabel() {
if (this.databaseEngine === "mongo") {
if (!this.selectedMongoDatabaseItem) {
return "刷新库";
}
if (!this.selectedMongoCollectionItem) {
return "刷新集合";
}
return this.mongoAdmin.activeTab === "aggregate" ? "刷新聚合" : "刷新结果";
}
return this.selectedMySqlTableItem ? "刷新结果" : "刷新库";
},
mySqlRowsRangeText() {
if (!this.mySqlAdmin.rows.totalRows || this.mySqlAdmin.rows.items.length === 0) {
return "0 / 0";
}
const start = (this.mySqlAdmin.rows.page - 1) * this.mySqlAdmin.rows.pageSize + 1;
const end = start + this.mySqlAdmin.rows.items.length - 1;
return `${start}-${end} / ${this.mySqlAdmin.rows.totalRows}`;
},
mySqlOverviewStatusTone() {
if (this.mySqlAdmin.error || !this.mySqlAdmin.overview.ok) {
return "bad";
}
return (this.mySqlAdmin.overview.warnings || []).length > 0 ? "warn" : "ok";
},
mySqlOverviewStatusText() {
if (this.mySqlAdmin.error || !this.mySqlAdmin.overview.ok) {
return "存在错误";
}
return (this.mySqlAdmin.overview.warnings || []).length > 0 ? "权限受限" : "已连接";
},
mySqlOverviewWarningText() {
const warnings = this.mySqlAdmin.overview.warnings || [];
if (!warnings.length) {
return "";
}
if (warnings.length === 1) {
return warnings[0];
}
return `${warnings[0]}(另 ${warnings.length - 1} 项)`;
},
mySqlOverviewCards() {
const overview = this.mySqlAdmin.overview || {};
const info = overview.info || {};
const connections = overview.connections || {};
const throughput = overview.throughput || {};
const summary = overview.summary || {};
const replication = overview.replication || {};
return [
{
key: "version",
label: "版本",
value: info.version || "-",
meta: info.versionComment || "-",
tone: overview.ok ? "ok" : "bad",
},
{
key: "connections",
label: "连接",
value: `${this.formatNumber(connections.current)} / ${this.formatNumber(connections.max)}`,
meta: `运行 ${this.formatNumber(connections.running)}`,
tone: overview.ok ? "neutral" : "bad",
},
{
key: "throughput",
label: "QPS / TPS",
value: `${throughput.qps ?? 0} / ${throughput.tps ?? 0}`,
meta: `采样 ${throughput.sampleSeconds ?? 0}s`,
tone: overview.ok ? "neutral" : "bad",
},
{
key: "storage",
label: "容量",
value: `${this.formatMb(summary.totalSizeMb || 0)}`,
meta: `${this.formatNumber(summary.databaseTotal || 0)} 库 / ${this.formatNumber(summary.tableTotal || 0)}`,
tone: overview.ok ? "neutral" : "bad",
},
{
key: "replication",
label: "主从",
value: this.mySqlReplicationLabel(replication),
meta: this.mySqlReplicationMeta(replication),
tone: this.mySqlReplicationTone(replication),
},
{
key: "uptime",
label: "运行时长",
value: this.formatDuration(info.uptimeSeconds),
meta: info.serverId ? `serverId ${info.serverId}` : "-",
tone: overview.ok ? "neutral" : "bad",
},
];
},
cdnAdminKeyword() {
return this.cdnAdmin.keyword.trim().toLowerCase();
},
cdnOverviewStatusTone() {
if (this.cdnAdmin.error || this.cdnAdmin.overview.error) {
return "bad";
}
if ((this.cdnAdmin.overview.warnings || []).length > 0) {
return "warn";
}
return this.cdnAdmin.loaded ? "ok" : "neutral";
},
cdnOverviewStatusText() {
if (this.cdnAdmin.error || this.cdnAdmin.overview.error) {
return "未配置";
}
if ((this.cdnAdmin.overview.warnings || []).length > 0) {
return "部分异常";
}
return this.cdnAdmin.loaded ? "可用" : "待加载";
},
cdnAllowedDomains() {
return this.cdnAdmin.overview.settings?.allowedDomains || [];
},
cdnFormItemCount() {
return this.parseMultilineItems(this.cdnAdmin.form.itemsText).length;
},
cdnFormPlaceholder() {
if (this.cdnAdmin.form.purgeType === "path") {
return "https://h5.haiyihy.com/static/\nhttps://h5.haiyihy.com/assets/";
}
return "https://h5.haiyihy.com/index.html\nhttps://h5.haiyihy.com/assets/app.js";
},
cdnQuotaCards() {
const rows = [
...(this.cdnAdmin.overview.quota?.url || []).map((item) => ({ ...item, kind: "url" })),
...(this.cdnAdmin.overview.quota?.path || []).map((item) => ({ ...item, kind: "path" })),
];
return rows.map((item) => ({
key: `${item.kind}-${item.area || "default"}`,
label: `${item.kind === "url" ? "URL" : "目录"} / ${this.cdnAreaLabel(item.area || "auto")}`,
value: `${this.formatNumber(item.available)} / ${this.formatNumber(item.total)}`,
meta: `单次 ${this.formatNumber(item.batch)}`,
tone: Number(item.available || 0) > 0 ? "neutral" : "warn",
}));
},
filteredCdnTasks() {
const keyword = this.cdnAdminKeyword;
return (this.cdnAdmin.overview.tasks || []).filter((item) => (
!keyword
|| [
item.taskId,
item.url,
item.status,
item.purgeType,
item.flushType,
item.createTime,
]
.join(" ")
.toLowerCase()
.includes(keyword)
));
},
chatappCronSettings() {
return this.chatappCron.overview.settings || createChatappCronDeployState().overview.settings;
},
chatappCronMetadata() {
return this.chatappCron.overview.metadata || {};
},
chatappCronIsRunning() {
return Boolean(
this.chatappCron.deploying
|| this.chatappCron.status === "queued"
|| this.chatappCron.status === "running",
);
},
chatappCronStatusTone() {
if (this.chatappCron.error || this.chatappCron.status === "failed" || this.chatappCronMetadata.error) {
return "bad";
}
if (this.chatappCronIsRunning) {
return "warn";
}
if (this.chatappCron.status === "success" || this.chatappCron.overview.enabled) {
return "ok";
}
if (this.chatappCron.loaded && !this.chatappCron.overview.enabled) {
return "warn";
}
return "neutral";
},
chatappCronStatusText() {
if (this.chatappCron.error || this.chatappCron.status === "failed") {
return "部署异常";
}
if (this.chatappCron.status === "queued") {
return "排队中";
}
if (this.chatappCron.status === "running" || this.chatappCron.deploying) {
return "部署中";
}
if (this.chatappCron.status === "success") {
return "已完成";
}
if (this.chatappCron.loaded && !this.chatappCron.overview.enabled) {
return "未配置";
}
return this.chatappCron.loaded ? "可部署" : "待加载";
},
chatappCronCurrentCommit() {
return this.chatappCronMetadata.commit || this.chatappCron.result?.build?.commit || "";
},
chatappCronLogText() {
return (this.chatappCron.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");
},
recentChatappCronLogs() {
return (this.chatappCron.logs || [])
.slice(-5)
.reverse()
.map((entry, index) => ({
...entry,
key: `${entry.time || "log"}-${entry.stage || "stage"}-${index}`,
}));
},
chatappCronLogDownloadUrl() {
if (!this.chatappCron.operationId) {
return "";
}
const query = new URLSearchParams({ id: this.chatappCron.operationId });
return `/api/chatapp-cron/deploy-log?${query.toString()}`;
},
commonToolContextLabel() {
if (this.commonTool === "mysql") {
return "MySQL / 数据 / 写入 / 结构 / SQL";
}
if (this.commonTool === "cdn") {
return "CDN / URL 刷新 / 目录刷新 / 最近记录";
}
return "chatapp-cron / 构建 / 上传 / 重启";
},
mySqlSqlShortcuts() {
const tableName = this.selectedMySqlTableItem?.name || "";
const quotedTableName = tableName ? this.quoteMySqlIdentifier(tableName) : "";
const quotedNewTableName = this.quoteMySqlIdentifier(tableName ? `${tableName}_new` : "new_table");
const selectedDatabase = this.mySqlAdmin.selectedDatabase || "";
return [
{
id: "show-databases",
label: "SHOW DATABASES",
sql: "SHOW DATABASES",
disabled: false,
},
{
id: "create-database-template",
label: "CREATE DB",
sql: "CREATE DATABASE `new_database` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci",
disabled: false,
},
{
id: "drop-database-template",
label: "DROP DB",
sql: selectedDatabase ? `DROP DATABASE ${this.quoteMySqlIdentifier(selectedDatabase)}` : "",
disabled: !selectedDatabase,
},
{
id: "show-tables",
label: "SHOW TABLES",
sql: "SHOW TABLES",
disabled: !this.mySqlAdmin.selectedDatabase,
},
{
id: "show-create-database",
label: "SHOW CREATE DB",
sql: selectedDatabase ? `SHOW CREATE DATABASE ${this.quoteMySqlIdentifier(selectedDatabase)}` : "",
disabled: !selectedDatabase,
},
{
id: "describe",
label: "DESC",
sql: quotedTableName ? `DESC ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "count",
label: "COUNT",
sql: quotedTableName ? `SELECT COUNT(*) AS total FROM ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "sample",
label: "SELECT 100",
sql: quotedTableName ? `SELECT * FROM ${quotedTableName} LIMIT 100` : "",
disabled: !quotedTableName,
},
{
id: "create",
label: "SHOW CREATE",
sql: quotedTableName ? `SHOW CREATE TABLE ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "status",
label: "TABLE STATUS",
sql: tableName ? `SHOW TABLE STATUS LIKE '${this.escapeMySqlString(tableName)}'` : "",
disabled: !tableName,
},
{
id: "indexes",
label: "SHOW INDEX",
sql: quotedTableName ? `SHOW INDEX FROM ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "show-columns",
label: "SHOW COLUMNS",
sql: quotedTableName ? `SHOW FULL COLUMNS FROM ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "explain",
label: "EXPLAIN",
sql: quotedTableName ? `EXPLAIN SELECT * FROM ${quotedTableName} LIMIT 100` : "",
disabled: !quotedTableName,
},
{
id: "processlist",
label: "PROCESSLIST",
sql: "SHOW FULL PROCESSLIST",
disabled: false,
},
{
id: "replication",
label: "READ ONLY",
sql: "SHOW GLOBAL VARIABLES LIKE 'read_only'",
disabled: false,
},
{
id: "insert-template",
label: "INSERT 模板",
sql: quotedTableName ? `INSERT INTO ${quotedTableName} () VALUES ()` : "",
disabled: !quotedTableName,
},
{
id: "update-template",
label: "UPDATE 模板",
sql: quotedTableName ? `UPDATE ${quotedTableName} SET WHERE LIMIT 1` : "",
disabled: !quotedTableName,
},
{
id: "delete-template",
label: "DELETE 模板",
sql: quotedTableName ? `DELETE FROM ${quotedTableName} WHERE LIMIT 1` : "",
disabled: !quotedTableName,
},
{
id: "add-column-template",
label: "ADD COLUMN",
sql: quotedTableName ? `ALTER TABLE ${quotedTableName} ADD COLUMN \`new_column\` VARCHAR(255) NULL` : "",
disabled: !quotedTableName,
},
{
id: "add-index-template",
label: "CREATE INDEX",
sql: quotedTableName ? `CREATE INDEX \`idx_${tableName || "table"}_column\` ON ${quotedTableName} (\`column\`)` : "",
disabled: !quotedTableName,
},
{
id: "create-table-template",
label: "CREATE TABLE",
sql: `CREATE TABLE ${quotedNewTableName} (\n \`id\` BIGINT NOT NULL AUTO_INCREMENT,\n PRIMARY KEY (\`id\`)\n)`,
disabled: false,
},
{
id: "rename-table-template",
label: "RENAME TABLE",
sql: quotedTableName ? `RENAME TABLE ${quotedTableName} TO ${quotedNewTableName}` : "",
disabled: !quotedTableName,
},
{
id: "truncate-template",
label: "TRUNCATE",
sql: quotedTableName ? `TRUNCATE TABLE ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "drop-table-template",
label: "DROP TABLE",
sql: quotedTableName ? `DROP TABLE ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "analyze-table-template",
label: "ANALYZE TABLE",
sql: quotedTableName ? `ANALYZE TABLE ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "optimize-table-template",
label: "OPTIMIZE TABLE",
sql: quotedTableName ? `OPTIMIZE TABLE ${quotedTableName}` : "",
disabled: !quotedTableName,
},
{
id: "use-database",
label: "USE",
sql: selectedDatabase ? `USE ${this.quoteMySqlIdentifier(selectedDatabase)}` : "",
disabled: !selectedDatabase,
},
];
},
tatAlertHosts() {
return this.allHostRows.filter((host) => (
host.hasMetricIssue
&& (
!this.normalizedInfraKeyword
|| host.searchText.includes(this.normalizedInfraKeyword)
)
));
},
tatHealthyHosts() {
return this.allHostRows.filter((host) => (
!host.hasMetricIssue
&& (
!this.normalizedInfraKeyword
|| host.searchText.includes(this.normalizedInfraKeyword)
)
));
},
visibleTatHealthyHosts() {
return this.infraHealthyTatOpen ? this.tatHealthyHosts : [];
},
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;
},
allReleaseTargetRows() {
return (this.updateOps.items || [])
.map((item) => this.decorateReleaseTargetItem(item))
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
},
releaseTargetRows() {
const keyword = this.normalizedReleaseKeyword;
return this.allReleaseTargetRows
.filter((item) => {
if (keyword && !item.searchText.includes(keyword)) {
return false;
}
return true;
});
},
restartTargetRows() {
const keyword = this.normalizedReleaseKeyword;
return (this.restartOps.items || [])
.map((item) => ({
...item,
hostCount: (item.hosts || []).length,
hostsLabel: (item.hosts || []).map((host) => host.host).join(" / "),
searchText: [
item.service,
item.kind,
...(item.hosts || []).flatMap((host) => [host.host, host.ip, host.path, host.port]),
].join(" ").toLowerCase(),
}))
.filter((item) => {
if (keyword && !item.searchText.includes(keyword)) {
return false;
}
return true;
})
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
},
releaseStatusSummary() {
const affectedServices = this.updateOps.result?.services?.length
|| this.updateOps.selectedServices.length
|| 0;
const lastLog = (this.updateOps.logs || []).slice(-1)[0];
return [
{
label: "上次执行",
value: this.formatDateTime(this.updateOps.finishedAt || this.updateOps.startedAt),
tone: "neutral",
},
{
label: "最近结果",
value: this.releasePrimaryStatusLabel(),
tone: this.releasePrimaryStatusTone(),
},
{
label: "失败阶段",
value: this.updateOps.error || this.restartOps.error ? (this.updateOps.stage || "failed") : "-",
tone: this.updateOps.error || this.restartOps.error ? "bad" : "neutral",
},
{
label: "影响服务",
value: String(affectedServices),
tone: affectedServices > 0 ? "warn" : "neutral",
},
{
label: "最近日志",
value: lastLog?.message ? this.truncateMiddle(lastLog.message, 24, 14) : "-",
tone: lastLog?.level === "error" ? "bad" : "neutral",
},
];
},
releaseHistoryRows() {
return this.releaseHistory.slice(0, 8);
},
configContextLabel() {
if (this.configTab === "nacos") {
return `Nacos / ${this.nacosConfig.namespaceName || "default"} / ${this.filteredNacosConfigs.length}`;
}
if (this.configTab === "go") {
return `Go Env / ${this.goConfig.hosts.length} 台 / ${this.goConfig.diffKeys.length} 差异`;
}
return `历史版本 / ${this.configHistoryRows.length}`;
},
updateAffectedServicesCount() {
return this.updateOps.result?.services?.length || this.updateOps.selectedServices.length || 0;
},
overviewStatusSegments() {
const serviceDownTotal = this.allServiceRows.filter((row) => row.downCount > 0).length;
const instanceDownTotal = this.summary.down || 0;
const hostAlertTotal = this.overviewAlertHosts.length;
const hostMetricDown = this.summary.hostMetricDown || 0;
const hostTotal = this.summary.hostTotal || 0;
const nacosUnavailable = !(this.payload.nacos && this.payload.nacos.ok);
const nacosUnhealthyTotal = this.summary.nacosUnhealthyInstanceTotal || 0;
const mongoUnavailable = !(this.payload.mongo && this.payload.mongo.ok);
const hasCriticalAlert = serviceDownTotal > 0 || instanceDownTotal > 0 || nacosUnavailable || mongoUnavailable;
const hasWarning = hostAlertTotal > 0 || hostMetricDown > 0 || nacosUnhealthyTotal > 0;
return [
{
key: "overall",
label: "状态",
value: hasCriticalAlert ? "存在异常" : hasWarning ? "需要关注" : "当前无异常",
meta: hasCriticalAlert
? `${serviceDownTotal} 服务 / ${instanceDownTotal} 实例`
: hasWarning
? `${hostAlertTotal} 主机 / ${nacosUnhealthyTotal} Nacos`
: `${this.allServiceRows.length} 服务稳定`,
tone: hasCriticalAlert ? "bad" : hasWarning ? "warn" : "ok",
},
{
key: "hosts",
label: "业务主机",
value: `${this.overviewHostRows.length}`,
meta: hostAlertTotal > 0 ? `${hostAlertTotal} 台异常` : `${this.overviewQuietServiceTotal} 服务在线`,
tone: hostAlertTotal > 0 ? (this.overviewAlertHosts.some((host) => host.downCount > 0) ? "bad" : "warn") : "ok",
},
{
key: "nacos",
label: "Nacos",
value: nacosUnavailable ? "连接异常" : nacosUnhealthyTotal > 0 ? `${nacosUnhealthyTotal} 异常` : "连接正常",
meta: `${this.payload.nacos?.summary?.serviceTotal || 0} 服务`,
tone: nacosUnavailable ? "bad" : nacosUnhealthyTotal > 0 ? "warn" : "ok",
},
{
key: "mongo",
label: "Mongo",
value: mongoUnavailable ? "连接异常" : `${this.summary.mongoDatabaseTotal || 0}`,
meta: mongoUnavailable ? "deploy 直连失败" : "连接正常",
tone: mongoUnavailable ? "bad" : "ok",
},
{
key: "tat",
label: "TAT",
value: `${this.summary.hostMetricOk || 0}/${hostTotal}`,
meta: hostMetricDown > 0 ? `${hostMetricDown} 台异常` : "采集正常",
tone: hostMetricDown > 0 ? "warn" : "ok",
},
];
},
overviewHealthyInfraCards() {
const cards = [];
if (this.payload.nacos && this.payload.nacos.ok && (this.summary.nacosUnhealthyInstanceTotal || 0) === 0) {
cards.push({
key: "nacos",
label: "Nacos",
value: "连接正常",
meta: `${this.payload.nacos.summary.serviceTotal || 0} 服务`,
tone: "ok",
});
}
if (this.payload.mongo && this.payload.mongo.ok) {
cards.push({
key: "mongo",
label: "Mongo",
value: `${this.summary.mongoDatabaseTotal || 0}`,
meta: "连接正常",
tone: "ok",
});
}
if ((this.summary.hostMetricDown || 0) === 0 && (this.summary.hostTotal || 0) > 0) {
cards.push({
key: "tat",
label: "TAT",
value: `${this.summary.hostMetricOk || 0}/${this.summary.hostTotal || 0}`,
meta: "采集正常",
tone: "ok",
});
}
return cards;
},
overviewHealthyHostGroups() {
const groups = new Map();
this.overviewQuietHosts.forEach((host) => {
if (!groups.has(host.group)) {
groups.set(host.group, {
key: host.group,
label: host.groupLabel,
hosts: [],
});
}
groups.get(host.group).hosts.push(host.host);
});
return [...groups.values()]
.map((group) => ({
...group,
count: group.hosts.length,
summary: group.hosts.join(" / "),
}))
.sort((left, right) => this.groupRank(left.key) - this.groupRank(right.key));
},
overviewHealthyStateCount() {
return this.overviewHealthyInfraCards.length + this.overviewHealthyHostGroups.length;
},
overviewQuietServiceTotal() {
return this.overviewQuietHosts.reduce((total, host) => total + (host.serviceTotal || 0), 0);
},
overviewDetailSubtitle() {
if (this.showSelectedServiceDetail) {
return "";
}
if (this.activeView === "overview") {
if (this.alertFeed.length > 0) {
return `${this.alertFeed.length} 条异常 · ${this.overviewHealthyStateCount} 组正常状态`;
}
if (this.overviewHealthyStateCount > 0) {
return `${this.overviewHealthyStateCount} 组正常状态`;
}
return "当前没有可展示的状态";
}
return `${this.alertFeed.length} 条异常`;
},
overviewHostPanelSummary() {
if (this.overviewAlertHosts.length > 0) {
return `${this.overviewAlertHosts.length} 台异常业务主机 · ${this.overviewQuietHosts.length} 台正常主机`;
}
return `${this.overviewQuietHosts.length} 台业务主机稳定`;
},
showSelectedServiceDetail() {
if (!this.selectedServiceDetail) {
return false;
}
if (this.activeView === "services") {
return true;
}
return (
this.selectedServiceDetail.downCount > 0
|| Boolean(this.focusedHostKey)
|| this.filter === "down"
|| Boolean(this.normalizedKeyword)
);
},
overviewBridgeAlerts() {
return this.serviceMatrixRows
.filter((row) => row.downCount > 0)
.map((row) => ({
key: row.key,
service: row.service,
groupLabel: row.groupLabel,
downCount: row.downCount,
hosts: row.instances
.filter((instance) => !instance.ok)
.map((instance) => instance.host),
}))
.slice(0, 12);
},
overviewInfraCards() {
const nacosTone = !(this.payload.nacos && this.payload.nacos.ok)
? "bad"
: (this.summary.nacosUnhealthyInstanceTotal || 0) > 0
? "warn"
: "ok";
const mongoTone = this.payload.mongo && this.payload.mongo.ok ? "ok" : "bad";
const tatTone = (this.summary.hostMetricDown || 0) > 0 ? "warn" : "ok";
return [
{
key: "nacos",
view: "infra",
label: "Nacos",
badge: this.payload.nacos && this.payload.nacos.ok ? "连接正常" : "连接异常",
tone: nacosTone,
meta: this.payload.nacos.baseUrl || "deploy 直连",
metrics: [
{ label: "服务", value: this.payload.nacos.summary.serviceTotal || 0 },
{ label: "实例", value: this.payload.nacos.summary.instanceTotal || 0 },
{ label: "异常", value: this.payload.nacos.summary.unhealthyInstanceTotal || 0 },
],
},
{
key: "mongo",
view: "infra",
label: "MongoDB",
badge: this.payload.mongo && this.payload.mongo.ok ? "连接正常" : "连接异常",
tone: mongoTone,
meta: this.payload.mongo.info.host || "deploy 直连",
metrics: [
{ label: "数据库", value: this.payload.mongo.summary.databaseTotal || 0 },
{ label: "集合", value: this.payload.mongo.summary.collectionTotal || 0 },
{ label: "连接", value: this.payload.mongo.connections.current ?? 0 },
],
},
{
key: "tat",
view: "hosts",
label: "TAT",
badge: (this.summary.hostMetricDown || 0) > 0 ? "采集异常" : "采集正常",
tone: tatTone,
meta: "主机资源采集",
metrics: [
{ label: "主机", value: this.summary.hostTotal || 0 },
{ label: "正常", value: this.summary.hostMetricOk || 0 },
{ label: "异常", value: this.summary.hostMetricDown || 0 },
],
},
];
},
alertFeed() {
const alerts = [];
this.allServiceRows.forEach((row) => {
row.instances
.filter((instance) => !instance.ok)
.forEach((instance) => {
alerts.push({
key: `service-${row.service}-${instance.host}`,
type: "service",
service: row.service,
tone: "bad",
title: row.service,
text: `${instance.host} · ${instance.detail || "HTTP 异常"}`,
});
});
});
this.allHostRows
.filter((host) => host.hasMetricIssue)
.forEach((host) => {
alerts.push({
key: `host-${host.host}`,
type: "host",
host: host.host,
tone: "warn",
title: host.host,
text: host.metrics.error || "主机指标异常",
});
});
if (this.payload.nacos.error || !(this.payload.nacos && this.payload.nacos.ok)) {
alerts.push({
key: "nacos-error",
type: "infra",
view: "infra",
tone: "bad",
title: "Nacos",
text: this.payload.nacos.error || "连接异常",
});
}
if (this.payload.mongo.error || !(this.payload.mongo && this.payload.mongo.ok)) {
alerts.push({
key: "mongo-error",
type: "infra",
view: "infra",
tone: "bad",
title: "MongoDB",
text: this.payload.mongo.error || "连接异常",
});
}
return alerts.slice(0, 5);
},
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");
},
recentUpdateLogs() {
return (this.updateOps.logs || [])
.slice(-5)
.reverse()
.map((entry, index) => ({
...entry,
key: `${entry.time || "log"}-${entry.stage || "stage"}-${index}`,
}));
},
serviceLogDownloadUrl() {
if (!this.serviceLog.service || !this.serviceLog.host) {
return "";
}
const requested = Number(this.serviceLog.exportLines || this.serviceLog.previewLines || 1000);
const fallback = Number(this.serviceLog.previewLines || 1000);
const lines = Math.min(
Math.max(1, Number.isNaN(requested) ? fallback : requested),
Number(this.serviceLog.exportMaxLines || 10000),
);
const query = new URLSearchParams({
service: this.serviceLog.service,
host: this.serviceLog.host,
lines: String(lines),
});
return `/api/ops/service-log/export?${query.toString()}`;
},
activeLogViewerTitle() {
if (this.logViewer.kind === "service") {
return `${this.serviceLog.service || "-"} · ${this.serviceLog.host || "-"}`;
}
if (this.logViewer.kind === "chatapp-cron") {
return "chatapp-cron 部署日志";
}
return "执行日志";
},
activeLogViewerSubtitle() {
if (this.logViewer.kind === "service") {
const subtitleParts = [];
if (this.serviceLog.container) {
subtitleParts.push(this.serviceLog.container);
}
subtitleParts.push(`更新于 ${this.formatDateTime(this.serviceLog.updatedAt)}`);
return subtitleParts.join(" · ");
}
if (this.logViewer.kind === "chatapp-cron") {
return `任务 ${this.chatappCron.operationId || "-"} · ${this.chatappCronStatusText} · ${this.formatDateTime(this.chatappCron.finishedAt || this.chatappCron.startedAt)}`;
}
return `任务 ${this.updateOps.operationId || "-"} · ${this.updateStatusLabel()} · ${this.formatDateTime(this.updateOps.finishedAt || this.updateOps.startedAt)}`;
},
activeLogViewerBadge() {
if (this.logViewer.kind === "service") {
return this.serviceLog.container || "";
}
if (this.logViewer.kind === "chatapp-cron") {
return this.chatappCron.operationId || "";
}
return this.updateOps.operationId || "";
},
activeLogViewerSecondaryBadge() {
if (this.logViewer.kind === "service") {
return `最近 ${this.serviceLog.previewLines}`;
}
if (this.logViewer.kind === "chatapp-cron") {
return this.chatappCron.stage ? `阶段 ${this.chatappCron.stage}` : "";
}
return this.updateOps.stage ? `阶段 ${this.updateOps.stage}` : "";
},
activeLogViewerContent() {
if (this.logViewer.kind === "service") {
return this.serviceLog.content;
}
if (this.logViewer.kind === "chatapp-cron") {
return this.chatappCronLogText;
}
return this.updateLogText;
},
activeLogViewerError() {
if (this.logViewer.kind === "service") {
return this.serviceLog.error;
}
if (this.logViewer.kind === "chatapp-cron") {
return this.chatappCron.error ? this.explainError(this.chatappCron.error) : "";
}
return this.updateOps.error ? this.explainError(this.updateOps.error) : "";
},
activeLogViewerLoading() {
if (this.logViewer.kind === "service") {
return this.serviceLog.loading;
}
if (this.logViewer.kind === "chatapp-cron") {
return this.chatappCron.statusRequestInFlight;
}
return this.updateOps.statusRequestInFlight;
},
activeLogViewerExporting() {
if (this.logViewer.kind === "service") {
return this.serviceLog.exporting;
}
return false;
},
activeLogViewerRefreshDisabled() {
if (this.logViewer.kind === "service") {
return !this.serviceLog.service || !this.serviceLog.host || this.serviceLog.loading;
}
if (this.logViewer.kind === "chatapp-cron") {
return !this.chatappCron.operationId || this.chatappCron.statusRequestInFlight;
}
return !this.updateOps.operationId || this.updateOps.statusRequestInFlight;
},
activeLogViewerDownloadDisabled() {
if (this.logViewer.kind === "service") {
return !this.serviceLog.service || !this.serviceLog.host || this.serviceLog.exporting;
}
if (this.logViewer.kind === "chatapp-cron") {
return !this.chatappCron.operationId;
}
return !this.updateOps.operationId;
},
},
watch: {
refreshIntervalSeconds() {
window.localStorage.setItem(STORAGE_KEY, String(this.refreshIntervalSeconds));
this.applyRefreshTimer();
},
activeView(nextValue) {
if (nextValue === "services") {
this.detailPanelOpen = true;
}
if (nextValue === "common") {
this.ensureCommonWorkbenchLoaded();
}
if (nextValue === "database") {
this.ensureDatabaseWorkbenchLoaded();
}
if (nextValue === "release") {
this.ensureReleaseWorkbenchLoaded();
}
if (nextValue === "config") {
this.configTab = this.configTab || "nacos";
}
if (nextValue === "infra") {
this.infraTab = this.infraTab || "overview";
}
},
selectedServiceKey() {
this.syncServiceLogTarget();
},
databaseEngine() {
if (this.activeView === "database") {
this.ensureDatabaseWorkbenchLoaded();
}
},
commonTool() {
if (this.activeView === "common") {
this.ensureCommonWorkbenchLoaded();
}
},
},
methods: {
async ensureCommonWorkbenchLoaded(tool = this.commonTool) {
if (tool === "mysql") {
if (!this.mySqlAdmin.loaded && !this.mySqlAdmin.loading) {
await this.loadMySqlWorkbench();
}
return;
}
if (tool === "cdn") {
if (!this.cdnAdmin.loaded && !this.cdnAdmin.loading) {
await this.refreshCdnOverview({ silent: true });
}
return;
}
if (tool === "cron" && !this.chatappCron.loaded && !this.chatappCron.loading) {
await this.refreshChatappCronOverview({ silent: true });
}
},
async ensureReleaseWorkbenchLoaded({ force = false } = {}) {
const shouldRefreshUpdate = force
|| ((!this.updateOps.loaded || this.updateOps.items.length === 0 || Boolean(this.updateOps.error))
&& !this.updateOps.loading);
const shouldRefreshRestart = force
|| ((!this.restartOps.loaded || this.restartOps.items.length === 0 || Boolean(this.restartOps.error))
&& !this.restartOps.loading);
const jobs = [];
if (shouldRefreshUpdate) {
jobs.push(this.refreshUpdateTargets());
}
if (shouldRefreshRestart) {
jobs.push(this.refreshRestartTargets());
}
if (jobs.length > 0) {
await Promise.allSettled(jobs);
}
this.runReleasePrecheck();
},
async ensureDatabaseWorkbenchLoaded(engine = this.databaseEngine) {
if (engine === "mongo") {
if (!this.mongoAdmin.loaded && !this.mongoAdmin.loading) {
await this.refreshMongoAdminOverview();
}
return;
}
if (!this.mySqlAdmin.loaded && !this.mySqlAdmin.loading) {
await this.loadMySqlWorkbench();
}
},
async switchDatabaseEngine(engine) {
if (!engine || this.databaseEngine === engine) {
return;
}
this.databaseEngine = engine;
if (this.activeView === "database") {
await this.ensureDatabaseWorkbenchLoaded(engine);
}
},
async switchCommonTool(tool) {
if (!tool || this.commonTool === tool) {
return;
}
this.commonTool = tool;
if (this.activeView === "common") {
await this.ensureCommonWorkbenchLoaded(tool);
}
},
async refreshDatabaseTree() {
if (this.databaseEngine === "mongo") {
await this.refreshMongoAdminOverview({ preserveSelection: true });
return;
}
await this.refreshMySqlDatabases({ preserveSelection: true });
},
async refreshDatabaseCurrent() {
if (this.databaseEngine === "mongo") {
await this.refreshMongoAdminCurrent();
return;
}
await this.refreshMySqlCurrent();
},
groupHosts(hostRows) {
const groups = new Map();
hostRows.forEach((host) => {
if (!groups.has(host.group)) {
groups.set(host.group, {
group: host.group,
label: this.groupLabel(host.group),
items: [],
alertCount: 0,
metricIssueCount: 0,
serviceCount: 0,
});
}
const target = groups.get(host.group);
target.items.push(host);
target.serviceCount += host.serviceTotal || 0;
if (host.hasAlert) {
target.alertCount += 1;
}
if (host.hasMetricIssue) {
target.metricIssueCount += 1;
}
});
return [...groups.values()].sort((left, right) => this.groupRank(left.group) - this.groupRank(right.group));
},
groupRank(group) {
return GROUP_ORDER[group] || 999;
},
groupLabel(group) {
return GROUP_LABELS[group] || group;
},
toneClass(level) {
return `tone-${level || "neutral"}`;
},
recordConfigHistory(scope, title, detail, tone = "neutral") {
this.configHistory = [
{
id: `config-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
scope,
title,
detail,
tone,
time: this.formatDateTime(new Date().toISOString()),
},
...this.configHistory,
].slice(0, 40);
},
recordReleaseHistory(scope, title, detail, tone = "neutral") {
this.releaseHistory = [
{
id: `release-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
scope,
title,
detail,
tone,
time: this.formatDateTime(new Date().toISOString()),
},
...this.releaseHistory,
].slice(0, 40);
},
explainError(message) {
const text = String(message || "").trim();
if (!text) {
return "";
}
if (text.includes("yaml renderer unavailable") || text.includes("No module named 'yaml'")) {
return "缺少 PyYAML当前无法渲染 compose 模板";
}
return text;
},
imageMeta(imageRef) {
const raw = String(imageRef || "").trim();
if (!raw) {
return {
repository: "",
repositoryShort: "-",
tag: "",
digestFull: "",
digestShort: "-",
};
}
const [withoutDigest, digestRaw = ""] = raw.split("@");
const lastSlash = withoutDigest.lastIndexOf("/");
const colon = withoutDigest.lastIndexOf(":");
const hasTag = colon > lastSlash;
const tag = hasTag ? withoutDigest.slice(colon + 1) : "";
const repository = hasTag ? withoutDigest.slice(0, colon) : withoutDigest;
const digestFull = digestRaw ? (digestRaw.startsWith("sha256:") ? digestRaw : `sha256:${digestRaw}`) : "";
return {
repository,
repositoryShort: this.truncateMiddle(repository, 18, 10),
tag,
digestFull,
digestShort: digestFull ? this.truncateMiddle(digestFull, 10, 6) : "-",
};
},
decorateReleaseTargetItem(item) {
const images = Array.isArray(item.currentImages) ? item.currentImages : [];
const primaryImage = images[0] || "";
const imageMeta = this.imageMeta(primaryImage);
const imageSummary = images.length > 1
? `${images.length} 个版本`
: primaryImage || "-";
const currentLabel = item.currentLabel || (images.length > 1 ? "多版本" : (imageMeta.tag || "-"));
const currentTitle = item.currentTitle || imageSummary;
const detailLabel = item.detailLabel || (images.length > 1 ? `${images.length}` : imageMeta.digestShort);
const detailTitle = item.detailTitle || primaryImage || "-";
const copyValue = item.copyValue || primaryImage || "";
return {
...item,
hostCount: (item.hosts || []).length,
hostsLabel: (item.hosts || []).map((host) => host.host).join(" / "),
imageTitle: images.join("\n"),
imageSummary,
currentLabel,
currentTitle,
detailLabel,
detailTitle,
copyValue,
digestFull: imageMeta.digestFull,
repositoryShort: item.repository ? this.truncateMiddle(item.repository, 18, 10) : imageMeta.repositoryShort,
currentImagePrimary: primaryImage,
currentImageCount: images.length,
searchText: [
item.service,
item.kind,
item.repository,
currentLabel,
detailLabel,
...(item.hosts || []).flatMap((host) => [host.host, host.ip, host.path, host.port]),
...images,
].join(" ").toLowerCase(),
};
},
truncateMiddle(value, start = 12, end = 10) {
const text = String(value || "");
if (!text) {
return "";
}
if (text.length <= start + end + 3) {
return text;
}
return `${text.slice(0, start)}...${text.slice(-end)}`;
},
async copyText(text, token = "") {
const value = String(text || "");
if (!value) {
return;
}
try {
await navigator.clipboard.writeText(value);
this.copiedToken = token || value;
if (this.copiedTokenTimer) {
window.clearTimeout(this.copiedTokenTimer);
}
this.copiedTokenTimer = window.setTimeout(() => {
this.copiedToken = "";
this.copiedTokenTimer = null;
}, 1600);
} catch (error) {
this.recordReleaseHistory("copy", "复制失败", error instanceof Error ? error.message : String(error), "bad");
}
},
isCopied(token) {
return this.copiedToken === token;
},
runReleasePrecheck() {
const selectedKinds = new Set(
(this.updateOps.items || [])
.filter((item) => this.updateOps.selectedServices.includes(item.service))
.map((item) => item.kind)
);
const checks = [
{
ok: this.updateOps.items.length > 0,
detail: this.updateOps.items.length > 0 ? `${this.updateOps.items.length} 项可更新服务` : this.explainError(this.updateOps.error) || "暂无可更新服务",
},
{
ok: this.updateOps.selectedServices.length > 0,
detail: this.updateOps.selectedServices.length > 0 ? this.updateOps.selectedServices.join(", ") : "未选择更新服务",
},
{
ok: this.restartOps.items.length > 0,
detail: this.restartOps.items.length > 0 ? `${this.restartOps.items.length} 项可重启服务` : (this.restartOps.error || "暂无重启目标"),
},
{
ok: (
(!selectedKinds.has("java") || Boolean(this.updateOps.javaGitRef.trim()))
&& (!selectedKinds.has("golang") || Boolean(this.updateOps.goGitRef.trim()))
&& (!selectedKinds.has("frontend") || Boolean(this.updateOps.adminGitRef.trim()))
),
detail: `Java ${this.updateOps.javaGitRef || "-"} / Go ${this.updateOps.goGitRef || "-"} / Admin ${this.updateOps.adminGitRef || "-"}`,
},
];
const ok = checks.every((item) => item.ok);
this.releasePrecheck = {
ok,
ranAt: new Date().toISOString(),
tone: ok ? "ok" : "warn",
title: ok ? "预检查通过" : "预检查未通过",
checks: [],
};
this.recordReleaseHistory("precheck", this.releasePrecheck.title, checks.map((item) => item.detail).join(" | "), ok ? "ok" : "warn");
},
async refreshReleaseTargets() {
await this.ensureReleaseWorkbenchLoaded({ force: true });
},
toggleConfigGroup(groupKey) {
const next = new Set(this.collapsedConfigGroups);
if (next.has(groupKey)) {
next.delete(groupKey);
} else {
next.add(groupKey);
}
this.collapsedConfigGroups = [...next];
},
syncConfigExplorerState(selectedKey = this.nacosConfig.selectedKey, { force = false } = {}) {
const groupKeys = [...new Set((this.nacosConfig.items || []).map((item) => item.group || "default"))];
if (groupKeys.length === 0) {
this.collapsedConfigGroups = [];
return;
}
const selectedItem = (this.nacosConfig.items || []).find((item) => this.configKey(item.group, item.dataId) === selectedKey);
const selectedGroup = selectedItem?.group || groupKeys[0];
if (force || this.collapsedConfigGroups.length === 0) {
this.collapsedConfigGroups = groupKeys.filter((groupKey) => groupKey !== selectedGroup);
return;
}
if (this.collapsedConfigGroups.includes(selectedGroup)) {
this.collapsedConfigGroups = this.collapsedConfigGroups.filter((groupKey) => groupKey !== selectedGroup);
}
},
isConfigGroupCollapsed(groupKey) {
return this.collapsedConfigGroups.includes(groupKey);
},
scrollSelectedConfigItemIntoView() {
this.$nextTick(() => {
const node = document.getElementById(`config-item-${this.nacosConfig.selectedKey}`);
if (node) {
node.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
});
},
openPrevNacosConfig() {
if (!this.selectedPrevNacosConfigKey) {
return;
}
this.nacosConfig.selectedKey = this.selectedPrevNacosConfigKey;
this.openNacosConfigByKey(this.selectedPrevNacosConfigKey);
},
openNextNacosConfig() {
if (!this.selectedNextNacosConfigKey) {
return;
}
this.nacosConfig.selectedKey = this.selectedNextNacosConfigKey;
this.openNacosConfigByKey(this.selectedNextNacosConfigKey);
},
toggleNacosService(serviceName) {
const next = new Set(this.expandedNacosServices);
if (next.has(serviceName)) {
next.delete(serviceName);
} else {
next.add(serviceName);
}
this.expandedNacosServices = [...next];
},
isNacosServiceExpanded(serviceName) {
return this.expandedNacosServices.includes(serviceName);
},
restoreNacosContent() {
this.nacosConfig.editor.content = this.nacosConfig.loadedContent;
this.recordConfigHistory("nacos", "恢复编辑内容", this.nacosConfig.editor.dataId || "-", "warn");
},
restoreGoContent() {
this.goConfig.editorContent = this.goConfig.loadedContent;
this.recordConfigHistory("go", "恢复 Go 配置", "已回退到当前线上版本", "warn");
},
splitLines(text) {
return String(text || "").replace(/\r\n/g, "\n").split("\n");
},
buildLineDiffRows(beforeText, afterText) {
const before = this.splitLines(beforeText);
const after = this.splitLines(afterText);
const rows = [];
const max = Math.max(before.length, after.length);
let leftNo = 1;
let rightNo = 1;
for (let index = 0; index < max; index += 1) {
const left = before[index];
const right = after[index];
if (left === right) {
if (left !== undefined) {
leftNo += 1;
rightNo += 1;
}
continue;
}
if (left !== undefined) {
rows.push({
key: `remove-${index}-${leftNo}`,
type: "remove",
leftNo,
rightNo: "",
text: left,
});
leftNo += 1;
}
if (right !== undefined) {
rows.push({
key: `add-${index}-${rightNo}`,
type: "add",
leftNo: "",
rightNo,
text: right,
});
rightNo += 1;
}
}
return rows;
},
toggleOverviewHealthyHosts() {
this.overviewHealthyHostsOpen = !this.overviewHealthyHostsOpen;
},
toggleDetailPanel() {
this.detailPanelOpen = !this.detailPanelOpen;
},
isSelectedService(serviceKey) {
return Boolean(this.selectedServiceDetail && this.selectedServiceDetail.key === serviceKey);
},
selectService(serviceKey) {
this.selectedServiceKey = serviceKey;
this.detailPanelOpen = true;
},
openUpdateLogViewer() {
this.logViewer.open = true;
this.logViewer.kind = "update";
this.$nextTick(() => this.scrollUpdateLogToBottom());
},
openChatappCronLogViewer() {
this.logViewer.open = true;
this.logViewer.kind = "chatapp-cron";
this.$nextTick(() => this.scrollUpdateLogToBottom());
},
closeLogViewer() {
const previousKind = this.logViewer.kind;
this.logViewer.open = false;
this.logViewer.kind = "";
if (previousKind === "service") {
this.clearServiceLog();
}
},
async refreshActiveLogViewer() {
if (this.logViewer.kind === "service") {
if (!this.serviceLog.host) {
return;
}
await this.openServiceLog({ host: this.serviceLog.host });
return;
}
if (this.logViewer.kind === "chatapp-cron") {
if (!this.chatappCron.operationId) {
return;
}
await this.fetchChatappCronDeployStatus(this.chatappCron.operationId);
this.scrollUpdateLogToBottom();
return;
}
if (!this.updateOps.operationId) {
return;
}
await this.fetchUpdateOperationStatus(this.updateOps.operationId);
this.scrollUpdateLogToBottom();
},
async downloadActiveLogViewer() {
if (this.logViewer.kind === "service") {
await this.downloadServiceLog();
return;
}
if (this.logViewer.kind === "chatapp-cron") {
if (!this.chatappCron.operationId) {
return;
}
try {
await this.downloadFile(this.chatappCronLogDownloadUrl, `chatapp-cron-${this.chatappCron.operationId}.log`);
} catch (error) {
this.chatappCron.error = error instanceof Error ? error.message : String(error);
}
return;
}
if (!this.updateOps.operationId) {
return;
}
try {
await this.downloadFile(this.updateLogDownloadUrl(), `service-update-${this.updateOps.operationId}.log`);
} catch (error) {
this.updateOps.error = error instanceof Error ? error.message : String(error);
}
},
clearServiceLog() {
this.serviceLog.loading = false;
this.serviceLog.exporting = false;
this.serviceLog.error = "";
this.serviceLog.service = "";
this.serviceLog.host = "";
this.serviceLog.container = "";
this.serviceLog.content = "";
this.serviceLog.updatedAt = "";
this.serviceLog.previewLines = 1000;
this.serviceLog.exportLines = 1000;
this.serviceLog.exportMaxLines = 10000;
},
syncServiceLogTarget() {
if (!this.serviceLog.service || !this.serviceLog.host) {
return;
}
if (!this.selectedServiceDetail || this.selectedServiceDetail.service !== this.serviceLog.service) {
if (this.logViewer.kind === "service" && this.logViewer.open) {
this.closeLogViewer();
} else {
this.clearServiceLog();
}
return;
}
const exists = (this.selectedServiceDetail.instances || []).some((instance) => instance.host === this.serviceLog.host);
if (!exists) {
if (this.logViewer.kind === "service" && this.logViewer.open) {
this.closeLogViewer();
} else {
this.clearServiceLog();
}
}
},
isServiceLogActive(instance) {
return this.serviceLog.service === (this.selectedServiceDetail?.service || "") && this.serviceLog.host === instance.host;
},
async openServiceLog(instance) {
const serviceName = this.selectedServiceDetail?.service || "";
const hostName = instance?.host || "";
if (!serviceName || !hostName) {
return;
}
this.logViewer.open = true;
this.logViewer.kind = "service";
this.serviceLog.loading = true;
this.serviceLog.error = "";
this.serviceLog.service = serviceName;
this.serviceLog.host = hostName;
this.serviceLog.container = "";
this.serviceLog.content = "";
try {
const query = new URLSearchParams({ service: serviceName, host: hostName });
const { response, payload } = await this.requestJson(`/api/ops/service-log?${query.toString()}`);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.serviceLog.service = payload.service || serviceName;
this.serviceLog.host = payload.host || hostName;
this.serviceLog.container = payload.container || "";
this.serviceLog.content = typeof payload.content === "string" ? payload.content : "";
this.serviceLog.updatedAt = payload.updatedAt || "";
this.serviceLog.previewLines = Number(payload.previewLines || payload.lines || 1000);
this.serviceLog.exportMaxLines = Number(payload.exportMaxLines || 10000);
this.serviceLog.exportLines = Math.min(
Math.max(1, Number(this.serviceLog.exportLines || this.serviceLog.previewLines)),
this.serviceLog.exportMaxLines,
);
this.$nextTick(() => this.scrollUpdateLogToBottom());
} catch (error) {
this.serviceLog.error = error instanceof Error ? error.message : String(error);
} finally {
this.serviceLog.loading = false;
}
},
clearFocusedHost() {
this.focusedHostKey = "";
},
selectHost(hostName) {
const next = new Set(this.expandedHosts);
if (next.has(hostName)) {
next.delete(hostName);
} else {
next.add(hostName);
}
this.expandedHosts = [...next];
this.focusedHostKey = this.focusedHostKey === hostName ? "" : hostName;
},
toggleHost(hostName) {
const next = new Set(this.expandedHosts);
if (next.has(hostName)) {
next.delete(hostName);
} else {
next.add(hostName);
}
this.expandedHosts = [...next];
},
isHostExpanded(hostName) {
return this.expandedHosts.includes(hostName);
},
isHostHighlighted(host) {
return this.focusedHostKey === host.host || this.selectedServiceHosts.has(host.host);
},
jumpToAlert(alert) {
if (alert.type === "service" && alert.service) {
this.selectService(alert.service);
this.activeView = "services";
return;
}
if (alert.type === "host" && alert.host) {
this.activeView = "hosts";
this.focusedHostKey = alert.host;
if (!this.expandedHosts.includes(alert.host)) {
this.expandedHosts = [...this.expandedHosts, alert.host];
}
return;
}
this.activeView = alert.view || "infra";
},
matrixCellTitle(cell) {
if (!cell.present) {
return "未部署";
}
const parts = [
cell.host || "",
`HTTP ${cell.statusCode || (cell.ok ? "UP" : "ERR")}`,
];
if (cell.latencyMs != null) {
parts.push(this.formatLatency(cell.latencyMs));
}
if (cell.detail) {
parts.push(cell.detail);
}
return parts.filter(Boolean).join(" · ");
},
matrixSectionDownCount(section) {
return (section?.rows || []).reduce((total, row) => total + (row.downCount || 0), 0);
},
hostMetricEntries(host) {
const metrics = host?.metrics || {};
return [
{
key: "cpu",
label: "CPU",
value: metrics.cpuPercent,
display: this.formatPercent(metrics.cpuPercent),
level: metrics.cpuLevel || this.thresholdTone("cpuPercent", metrics.cpuPercent),
},
{
key: "memory",
label: "MEM",
value: metrics.memoryPercent,
display: this.formatPercent(metrics.memoryPercent),
level: metrics.memoryLevel || this.thresholdTone("memoryPercent", metrics.memoryPercent),
},
{
key: "disk",
label: "DISK",
value: metrics.diskPercent,
display: this.formatPercent(metrics.diskPercent),
level: metrics.diskLevel || this.thresholdTone("diskPercent", metrics.diskPercent),
},
];
},
hostHasMetricData(host) {
const metrics = host?.metrics || {};
return ["cpuPercent", "memoryPercent", "diskPercent"].some((key) => metrics[key] != null && !Number.isNaN(Number(metrics[key])));
},
hostMetricStatusLabel(host) {
if (host?.metrics?.error) {
return "采集异常";
}
return "暂无指标";
},
thresholdTone(metricKey, value) {
if (value == null || Number.isNaN(Number(value))) {
return "neutral";
}
const rule = this.thresholds?.[metricKey];
if (!rule) {
return "neutral";
}
const amount = Number(value);
const bad = Number(rule.bad);
const warn = Number(rule.warn);
if (!Number.isNaN(bad) && amount >= bad) {
return "bad";
}
if (!Number.isNaN(warn) && amount >= warn) {
return "warn";
}
return "ok";
},
metricRingStyle(value, level = "neutral") {
const amount = value == null || Number.isNaN(Number(value))
? 0
: Math.max(0, Math.min(100, Number(value)));
const tone = level || "neutral";
return {
"--metric-value": `${amount}%`,
"--metric-color": `var(--${tone === "bad" ? "bad" : tone === "warn" ? "warn" : tone === "ok" ? "ok" : "line-strong"})`,
};
},
hostServiceSummary(host, { quiet = false } = {}) {
const source = quiet ? host.items : (host.alertItems.length > 0 ? host.alertItems : host.items);
const services = [...new Set((source || []).map((item) => item.service).filter(Boolean))];
if (services.length === 0) {
return host.hasMetricIssue ? "指标采集异常" : "无服务";
}
if (services.length <= 4) {
return services.join(" / ");
}
return `${services.slice(0, 4).join(" / ")} +${services.length - 4}`;
},
hostSummaryTone(host) {
if (host.downCount > 0) {
return "bad";
}
if (host.hasMetricIssue) {
return "warn";
}
return "neutral";
},
buildHostRows({ businessOnly = false, applyFilter = false, applyKeyword = false } = {}) {
const keyword = applyKeyword ? this.normalizedKeyword : "";
return (this.payload.hosts || [])
.filter((host) => !businessOnly || ((host.items || []).length > 0))
.map((host) => {
const metrics = host.metrics || {};
const items = (host.items || [])
.map((item) => ({
...item,
level: item.level || (item.ok ? "ok" : "bad"),
}))
.sort((left, right) => (
Number(right.ok === false) - Number(left.ok === false)
|| left.service.localeCompare(right.service, "zh-CN")
));
const alertItems = items.filter((item) => !item.ok);
const downCount = alertItems.length;
const hasMetricIssue = metrics.ok === false || Boolean(metrics.error);
const alertServices = [...new Set(alertItems.map((item) => item.service))];
const alertSummary = alertServices.length > 0
? this.hostServiceSummary({ items, alertItems, hasMetricIssue }, { quiet: false })
: hasMetricIssue
? "指标采集异常"
: "无服务异常";
const quietSummary = this.hostServiceSummary({ items, alertItems, hasMetricIssue }, { quiet: true });
const searchText = [
host.host,
host.ip,
host.group,
host.instanceId,
...items.flatMap((item) => [
item.service,
item.kind,
item.detail,
item.url,
item.port,
item.statusCode,
]),
]
.join(" ")
.toLowerCase();
return {
...host,
metrics,
items,
alertItems,
alertSummary,
quietSummary,
groupLabel: this.groupLabel(host.group),
serviceTotal: items.length,
downCount,
hasMetricIssue,
hasAlert: downCount > 0 || hasMetricIssue,
searchText,
};
})
.filter((host) => {
if (applyFilter) {
if (this.filter === "down" && !host.hasAlert) {
return false;
}
if (this.filter === "infra" && BUSINESS_GROUPS.has(host.group)) {
return false;
}
if (["gateway", "app", "pay"].includes(this.filter) && host.group !== this.filter) {
return false;
}
}
if (keyword && !host.searchText.includes(keyword)) {
return false;
}
return true;
})
.sort((left, right) => (
Number(right.hasAlert) - Number(left.hasAlert)
|| this.groupRank(left.group) - this.groupRank(right.group)
|| left.host.localeCompare(right.host, "zh-CN")
));
},
buildServiceRows({ applyFilter = false, applyKeyword = false, columns = null } = {}) {
const columnHosts = Array.isArray(columns) ? columns : this.businessHosts;
const keyword = applyKeyword ? this.normalizedKeyword : "";
const rowMap = new Map();
this.businessHosts.forEach((host) => {
(host.items || []).forEach((item) => {
const rowKey = item.service;
if (!rowMap.has(rowKey)) {
rowMap.set(rowKey, {
key: rowKey,
service: item.service,
kind: item.kind || "-",
group: item.group || host.group,
groupLabel: this.groupLabel(item.group || host.group),
deployedCount: 0,
downCount: 0,
okCount: 0,
latencyValues: [],
instanceMap: {},
instances: [],
});
}
const row = rowMap.get(rowKey);
const latencyMs = item.latencyMs == null || Number.isNaN(Number(item.latencyMs))
? null
: Number(item.latencyMs);
const instance = {
host: host.host,
ip: host.ip,
group: host.group,
groupLabel: host.groupLabel,
present: true,
ok: Boolean(item.ok),
level: item.level || (item.ok ? "ok" : "bad"),
statusCode: item.statusCode,
latencyMs,
port: item.port,
detail: item.detail || "",
url: item.url || "",
kind: item.kind || row.kind,
};
row.instanceMap[host.host] = instance;
row.instances.push(instance);
row.deployedCount += 1;
row.kind = item.kind || row.kind;
if (instance.ok) {
row.okCount += 1;
} else {
row.downCount += 1;
}
if (latencyMs != null) {
row.latencyValues.push(latencyMs);
}
});
});
return [...rowMap.values()]
.map((row) => {
const cells = columnHosts.map((host) => row.instanceMap[host.host] || {
host: host.host,
ip: host.ip,
group: host.group,
groupLabel: host.groupLabel,
present: false,
ok: false,
level: "neutral",
statusCode: null,
latencyMs: null,
port: null,
detail: "",
url: "",
});
const avgLatencyMs = row.latencyValues.length > 0
? Math.round(row.latencyValues.reduce((sum, value) => sum + value, 0) / row.latencyValues.length)
: null;
const searchText = [
row.service,
row.kind,
row.group,
...row.instances.flatMap((instance) => [
instance.host,
instance.ip,
instance.detail,
instance.url,
instance.port,
instance.statusCode,
]),
]
.join(" ")
.toLowerCase();
return {
...row,
cells,
avgLatencyMs,
searchText,
};
})
.filter((row) => {
if (this.focusedHostKey && !row.instanceMap[this.focusedHostKey]) {
return false;
}
if (applyFilter) {
if (this.filter === "down" && row.downCount === 0) {
return false;
}
if (this.filter === "infra") {
return false;
}
if (["gateway", "app", "pay"].includes(this.filter) && row.group !== this.filter) {
return false;
}
}
if (keyword && !row.searchText.includes(keyword)) {
return false;
}
return true;
})
.sort((left, right) => (
Number(right.downCount > 0) - Number(left.downCount > 0)
|| right.downCount - left.downCount
|| left.service.localeCompare(right.service, "zh-CN")
));
},
statusCellLabel(cell) {
if (!cell.present) {
return "";
}
if (cell.statusCode && Number(cell.statusCode) > 0) {
return String(cell.statusCode);
}
return cell.ok ? "UP" : "ERR";
},
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();
},
formatLatency(value) {
if (value == null || Number.isNaN(Number(value))) {
return "-";
}
return `${Math.round(Number(value))}ms`;
},
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 "未开始";
},
releaseIsRunning() {
return Boolean(
this.restartOps.running
|| this.updateOps.running
|| this.updateOps.status === "queued"
|| this.updateOps.status === "running"
);
},
releasePrimaryStatusLabel() {
if (this.restartOps.running) {
return "重启执行中";
}
if (this.updateOps.status === "queued") {
return "更新排队中";
}
if (this.updateOps.running || this.updateOps.status === "running") {
return "更新执行中";
}
if (this.restartOps.error) {
return "重启失败";
}
if (this.updateOps.error || this.updateOps.status === "failed") {
return "更新失败";
}
if (this.restartOps.result?.ok) {
return "重启已完成";
}
if (this.updateOps.status === "success") {
return "更新已完成";
}
return "待操作";
},
releasePrimaryStatusTone() {
if (this.updateOps.error || this.restartOps.error || this.updateOps.status === "failed") {
return "bad";
}
if (this.releaseIsRunning()) {
return "warn";
}
if (this.updateOps.status === "success" || this.restartOps.result?.ok) {
return "ok";
}
return "neutral";
},
releasePrecheckBadgeLabel() {
if (!this.releasePrecheck.ranAt) {
return "未预检查";
}
return this.releaseIsRunning() ? `上次${this.releasePrecheck.title}` : this.releasePrecheck.title;
},
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 || "";
}
if (operation.status === "success") {
this.recordReleaseHistory(
"update",
"服务更新完成",
`${(operation.result?.services || []).join(", ") || "-"} · ${this.updateOps.finishedAt || this.updateOps.updatedAt || "-"}`,
"ok",
);
} else if (operation.status === "failed") {
this.recordReleaseHistory(
"update",
"服务更新失败",
this.explainError(this.updateOps.error || operation.message || "服务更新失败"),
"bad",
);
}
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, payload } = await this.requestJson(`/api/ops/update-status?${query.toString()}`);
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);
}
},
handleUnauthorized() {
if (this.auth.redirecting) {
return;
}
this.auth.redirecting = true;
this.stopUpdatePolling();
this.stopChatappCronPolling();
if (this.timer) {
window.clearInterval(this.timer);
this.timer = null;
}
window.sessionStorage.removeItem(UPDATE_OPERATION_STORAGE_KEY);
window.location.replace(loginRedirectPath());
},
async authFetch(url, options = {}) {
const requestOptions = {
credentials: "same-origin",
...options,
};
if (typeof requestOptions.cache === "undefined") {
requestOptions.cache = "no-store";
}
const actualUrl = typeof url === "string" && url.startsWith("/") ? appPath(url) : url;
return fetch(actualUrl, requestOptions);
},
async requestJson(url, options = {}) {
const response = await this.authFetch(url, options);
const payload = await this.readJsonResponse(response);
if (response.status === 401) {
this.handleUnauthorized();
throw new Error(payload.error || "authentication required");
}
return { response, payload };
},
async refreshSession() {
this.auth.checking = true;
try {
const response = await this.authFetch("/api/auth/session");
const payload = await this.readJsonResponse(response);
if (response.status === 401) {
this.handleUnauthorized();
return false;
}
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.auth.username = payload.username || "";
return true;
} finally {
this.auth.checking = false;
}
},
async bootstrap() {
const root = document.getElementById("app");
const bootIndicator = document.getElementById("boot-indicator");
try {
const authenticated = await this.refreshSession();
if (!authenticated) {
return;
}
} catch (error) {
this.error = error instanceof Error ? error.message : String(error);
} finally {
if (root) {
root.removeAttribute("v-cloak");
}
if (bootIndicator) {
bootIndicator.hidden = true;
}
}
if (this.error) {
return;
}
if (CURRENT_APP_CONTEXT.mode === "common") {
await this.refresh();
this.applyRefreshTimer();
return;
}
await Promise.allSettled([
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) {
return;
}
await this.fetchUpdateOperationStatus(lastOperationId, { silent: true });
if (this.updateOps.operationId) {
this.startUpdatePolling();
}
},
async logout() {
if (this.auth.loggingOut) {
return;
}
this.auth.loggingOut = true;
try {
await this.authFetch("/api/auth/logout", {
method: "POST",
});
} finally {
this.handleUnauthorized();
}
},
async downloadFile(url, fallbackName) {
const response = await this.authFetch(url);
if (response.status === 401) {
this.handleUnauthorized();
return;
}
if (!response.ok) {
const contentType = response.headers.get("Content-Type") || "";
if (contentType.includes("application/json")) {
const payload = await this.readJsonResponse(response);
throw new Error(payload.error || `HTTP ${response.status}`);
}
throw new Error((await response.text()) || `HTTP ${response.status}`);
}
const blob = await response.blob();
const contentDisposition = response.headers.get("Content-Disposition") || "";
const matched = contentDisposition.match(/filename="?([^";]+)"?/i);
const fileName = matched?.[1] || fallbackName;
const objectUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(objectUrl);
},
async downloadServiceLog() {
if (!this.serviceLogDownloadUrl) {
return;
}
this.serviceLog.exporting = true;
this.serviceLog.error = "";
try {
await this.downloadFile(
this.serviceLogDownloadUrl,
`${this.serviceLog.service}-${this.serviceLog.host}-${this.serviceLog.exportLines}.log`,
);
} catch (error) {
this.serviceLog.error = error instanceof Error ? error.message : String(error);
} finally {
this.serviceLog.exporting = false;
}
},
async refreshMySqlModule() {
this.loading = true;
this.error = "";
this.mySqlAdmin.error = "";
try {
await this.refreshMySqlDatabases({ preserveSelection: true, silent: true });
this.error = this.mySqlAdmin.error || "";
} finally {
this.loading = false;
}
},
async refreshCommonModule() {
this.loading = true;
this.error = "";
this.mySqlAdmin.error = "";
this.cdnAdmin.error = "";
this.chatappCron.error = "";
try {
await Promise.allSettled([
this.refreshMySqlDatabases({ preserveSelection: true, silent: true }),
this.refreshCdnOverview({ silent: true }),
this.refreshChatappCronOverview({ silent: true }),
]);
this.error = this.mySqlAdmin.error || this.cdnAdmin.error || this.chatappCron.error || "";
} finally {
this.loading = false;
}
},
async refresh() {
if (CURRENT_APP_CONTEXT.mode === "common") {
await this.refreshCommonModule();
return;
}
this.loading = true;
this.error = "";
try {
const { response, payload } = await this.requestJson("/api/monitor/services");
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.payload = payload;
this.applyMySqlOverviewToPayload(payload.mysql || createMySqlOverviewState());
this.syncRefreshSettings();
if (!this.selectedServiceDetail && this.allServiceRows.length > 0) {
this.selectedServiceKey = this.allServiceRows[0].key;
}
if (this.selectedServiceKey && !this.allServiceRows.some((row) => row.key === this.selectedServiceKey)) {
this.selectedServiceKey = this.allServiceRows[0]?.key || "";
}
this.syncServiceLogTarget();
} 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, payload } = await this.requestJson("/api/nacos/configs");
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.nacosConfig.items = Array.isArray(payload.items) ? payload.items : [];
this.nacosConfig.baseUrl = payload.baseUrl || "";
this.nacosConfig.namespaceId = payload.namespaceId || "";
this.nacosConfig.namespaceName = payload.namespaceName || "";
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;
this.syncConfigExplorerState(nextKey, { force: !preserveSelection || this.collapsedConfigGroups.length === 0 });
const sameAsCurrent = this.configKey(this.nacosConfig.editor.group, this.nacosConfig.editor.dataId) === nextKey;
if (!sameAsCurrent || reloadSelected || !this.nacosConfig.loadedContent) {
await this.openNacosConfigByKey(nextKey);
}
this.recordConfigHistory("nacos", "刷新 Nacos 配置", `${this.nacosConfig.items.length}`, "neutral");
} catch (error) {
this.nacosConfig.error = error instanceof Error ? error.message : String(error);
this.recordConfigHistory("nacos", "刷新 Nacos 配置失败", this.explainError(this.nacosConfig.error), "bad");
} 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, payload } = await this.requestJson(`/api/nacos/config?${query.toString()}`);
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,
};
this.syncConfigExplorerState(configKey);
this.scrollSelectedConfigItemIntoView();
this.recordConfigHistory("nacos", "打开配置", `${item.group}/${item.dataId}`, "neutral");
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
this.recordConfigHistory("nacos", "打开配置失败", this.explainError(this.nacosConfig.detailError), "bad");
} 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, payload } = await this.requestJson("/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,
}),
});
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 || "格式校验通过";
this.recordConfigHistory("nacos", "校验通过", `${this.nacosConfig.editor.group}/${this.nacosConfig.editor.dataId}`, "ok");
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
this.recordConfigHistory("nacos", "校验失败", this.explainError(this.nacosConfig.detailError), "bad");
} 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, payload } = await this.requestJson("/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,
}),
});
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}`;
this.recordConfigHistory("nacos", "保存成功", `${this.nacosConfig.editor.group}/${this.nacosConfig.editor.dataId}`, "ok");
await this.refreshNacosConfigs({ preserveSelection: true, reloadSelected: true });
} catch (error) {
this.nacosConfig.detailError = error instanceof Error ? error.message : String(error);
this.recordConfigHistory("nacos", "保存失败", this.explainError(this.nacosConfig.detailError), "bad");
} finally {
this.nacosConfig.saving = false;
}
},
async refreshGoConfig() {
this.goConfig.loading = true;
this.goConfig.error = "";
this.goConfig.saveMessage = "";
try {
const { response, payload } = await this.requestJson("/api/runtime/golang-config");
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;
this.recordConfigHistory("go", "刷新 Go 配置", `${this.goConfig.hosts.length} 台主机`, "neutral");
} catch (error) {
this.goConfig.error = error instanceof Error ? error.message : String(error);
this.recordConfigHistory("go", "刷新 Go 配置失败", this.explainError(this.goConfig.error), "bad");
} finally {
this.goConfig.loading = false;
}
},
async validateGoConfig() {
this.goConfig.validating = true;
this.goConfig.error = "";
this.goConfig.validationMessage = "";
try {
const { response, payload } = await this.requestJson("/api/runtime/golang-config/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ content: this.goConfig.editorContent }),
});
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 || "格式校验通过";
this.recordConfigHistory("go", "校验通过", `${this.goConfig.diffKeys.length} 个差异 key`, "ok");
} catch (error) {
this.goConfig.error = error instanceof Error ? error.message : String(error);
this.recordConfigHistory("go", "校验失败", this.explainError(this.goConfig.error), "bad");
} finally {
this.goConfig.validating = false;
}
},
async saveGoConfig() {
this.goConfig.saving = true;
this.goConfig.error = "";
this.goConfig.saveMessage = "";
try {
const { response, payload } = await this.requestJson("/api/runtime/golang-config", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ content: this.goConfig.editorContent }),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.goConfig.loadedContent = this.goConfig.editorContent;
this.goConfig.saveMessage = payload.message || "golang 配置已保存";
this.recordConfigHistory("go", "保存成功", payload.message || "Go 配置已下发", "ok");
await this.refreshGoConfig();
} catch (error) {
this.goConfig.error = error instanceof Error ? error.message : String(error);
this.recordConfigHistory("go", "保存失败", this.explainError(this.goConfig.error), "bad");
} finally {
this.goConfig.saving = false;
}
},
applyMySqlOverviewToPayload(overview) {
const nextOverview = overview && typeof overview === "object"
? overview
: createMySqlOverviewState();
this.payload.mysql = nextOverview;
this.payload.summary.mysqlDatabaseTotal = Number(nextOverview.summary?.databaseTotal || 0);
this.payload.summary.mysqlTableTotal = Number(nextOverview.summary?.tableTotal || 0);
this.payload.summary.mysqlOk = nextOverview.ok ? 1 : 0;
this.mySqlAdmin.overview = nextOverview;
},
setMySqlAdminMessage(message, tone = "neutral") {
this.mySqlAdmin.message = message || "";
this.mySqlAdmin.messageTone = tone || "neutral";
},
setCdnAdminMessage(message, tone = "neutral") {
this.cdnAdmin.message = message || "";
this.cdnAdmin.messageTone = tone || "neutral";
},
setChatappCronMessage(message, tone = "neutral") {
this.chatappCron.message = message || "";
this.chatappCron.messageTone = tone || "neutral";
},
applyChatappCronOverview(payload) {
const defaults = createChatappCronDeployState().overview;
const nextSettings = {
...defaults.settings,
...(payload?.settings || {}),
};
this.chatappCron.overview = {
...defaults,
...(payload || {}),
settings: nextSettings,
metadata: payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : {},
};
this.chatappCron.loaded = true;
this.chatappCron.error = payload?.error || "";
if (!this.chatappCron.form.gitRef || this.chatappCron.form.gitRef === defaults.settings.defaultGitRef) {
this.chatappCron.form.gitRef = nextSettings.defaultGitRef || defaults.settings.defaultGitRef;
}
},
applyChatappCronOperation(operation) {
if (!operation) {
return;
}
this.chatappCron.operationId = operation.operationId || "";
this.chatappCron.status = operation.status || "";
this.chatappCron.stage = operation.stage || "";
this.chatappCron.startedAt = operation.startedAt || "";
this.chatappCron.finishedAt = operation.finishedAt || "";
this.chatappCron.logs = Array.isArray(operation.logs) ? operation.logs : [];
this.chatappCron.result = operation.result || null;
this.chatappCron.deploying = operation.status === "queued" || operation.status === "running";
if (operation.status === "success") {
this.chatappCron.error = "";
this.setChatappCronMessage(operation.message || "chatapp-cron 部署完成", "ok");
} else if (operation.status === "failed") {
this.chatappCron.error = operation.error || operation.result?.error || "chatapp-cron 部署失败";
this.setChatappCronMessage(this.explainError(this.chatappCron.error), "warn");
} else {
this.setChatappCronMessage(operation.message || "chatapp-cron 部署执行中", "neutral");
}
this.scrollUpdateLogToBottom();
},
stopChatappCronPolling() {
if (this.chatappCron.pollTimer) {
window.clearInterval(this.chatappCron.pollTimer);
this.chatappCron.pollTimer = null;
}
},
startChatappCronPolling() {
this.stopChatappCronPolling();
if (!this.chatappCron.operationId) {
return;
}
this.chatappCron.pollTimer = window.setInterval(() => {
this.fetchChatappCronDeployStatus(this.chatappCron.operationId, { silent: true });
}, 2000);
},
parseMultilineItems(text) {
return String(text || "")
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
},
cdnAreaLabel(area) {
if (area === "mainland") {
return "境内";
}
if (area === "overseas") {
return "境外";
}
if (area === "global") {
return "全球";
}
return "自动";
},
cdnTaskStatusLabel(status) {
if (status === "done") {
return "成功";
}
if (status === "process") {
return "进行中";
}
if (status === "fail") {
return "失败";
}
return status || "-";
},
cdnTaskStatusTone(status) {
if (status === "done") {
return "ok";
}
if (status === "process") {
return "warn";
}
if (status === "fail") {
return "bad";
}
return "neutral";
},
cdnPurgeTypeLabel(purgeType) {
return purgeType === "path" ? "目录" : "URL";
},
cdnFlushTypeLabel(flushType) {
if (flushType === "delete") {
return "delete";
}
if (flushType === "flush") {
return "flush";
}
return "-";
},
async refreshCdnOverview({ silent = false } = {}) {
this.cdnAdmin.loading = true;
if (!silent) {
this.cdnAdmin.error = "";
}
try {
const { response, payload } = await this.requestJson("/api/cdn/overview");
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.cdnAdmin.overview = {
...createCdnAdminState().overview,
...payload,
settings: {
...createCdnAdminState().overview.settings,
...(payload.settings || {}),
},
quota: {
url: Array.isArray(payload.quota?.url) ? payload.quota.url : [],
path: Array.isArray(payload.quota?.path) ? payload.quota.path : [],
},
tasks: Array.isArray(payload.tasks) ? payload.tasks : [],
};
this.cdnAdmin.loaded = true;
this.cdnAdmin.error = payload.error || "";
if (!silent) {
if (payload.error) {
this.setCdnAdminMessage(this.explainError(payload.error), "warn");
} else if ((payload.warnings || []).length > 0) {
this.setCdnAdminMessage("CDN 记录已刷新,部分接口异常", "warn");
} else {
this.setCdnAdminMessage("已刷新 CDN 记录", "neutral");
}
}
} catch (error) {
this.cdnAdmin.error = error instanceof Error ? error.message : String(error);
if (!silent) {
this.setCdnAdminMessage(this.explainError(this.cdnAdmin.error), "warn");
}
} finally {
this.cdnAdmin.loading = false;
}
},
clearCdnForm() {
this.cdnAdmin.form.itemsText = "";
this.cdnAdmin.form.area = "";
this.cdnAdmin.form.flushType = "flush";
this.cdnAdmin.form.urlEncode = false;
},
async submitCdnPurge() {
if (this.cdnAdmin.submitting) {
return;
}
const items = this.parseMultilineItems(this.cdnAdmin.form.itemsText);
if (items.length === 0) {
this.setCdnAdminMessage("至少填写一条 URL 或目录", "warn");
return;
}
this.cdnAdmin.submitting = true;
this.cdnAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/cdn/purge", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
purgeType: this.cdnAdmin.form.purgeType,
items,
area: this.cdnAdmin.form.area,
flushType: this.cdnAdmin.form.flushType,
urlEncode: this.cdnAdmin.form.urlEncode,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.cdnAdmin.lastResult = payload;
this.setCdnAdminMessage(`已提交 ${payload.itemCount || items.length} 条刷新`, "ok");
await this.refreshCdnOverview({ silent: true });
} catch (error) {
this.cdnAdmin.error = error instanceof Error ? error.message : String(error);
this.setCdnAdminMessage(this.explainError(this.cdnAdmin.error), "warn");
} finally {
this.cdnAdmin.submitting = false;
}
},
async refreshChatappCronOverview({ silent = false } = {}) {
this.chatappCron.loading = true;
if (!silent) {
this.chatappCron.error = "";
}
try {
const { response, payload } = await this.requestJson("/api/chatapp-cron/overview");
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.applyChatappCronOverview(payload);
if (!silent) {
if (!payload.enabled) {
this.setChatappCronMessage("chatapp-cron 部署未配置", "warn");
} else if (payload.metadata?.error) {
this.setChatappCronMessage("已加载配置,最近版本读取失败", "warn");
} else {
this.setChatappCronMessage("chatapp-cron 配置已刷新", "neutral");
}
}
} catch (error) {
this.chatappCron.error = error instanceof Error ? error.message : String(error);
if (!silent) {
this.setChatappCronMessage(this.explainError(this.chatappCron.error), "warn");
}
} finally {
this.chatappCron.loading = false;
}
},
async fetchChatappCronDeployStatus(operationId = this.chatappCron.operationId, { silent = false } = {}) {
if (!operationId || this.chatappCron.statusRequestInFlight) {
return;
}
const wasRunning = this.chatappCron.deploying;
this.chatappCron.statusRequestInFlight = true;
try {
const query = new URLSearchParams({ id: operationId });
const { response, payload } = await this.requestJson(`/api/chatapp-cron/deploy-status?${query.toString()}`);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
if (this.chatappCron.operationId && this.chatappCron.operationId !== operationId) {
return;
}
this.applyChatappCronOperation(payload.operation || {});
if (!this.chatappCron.deploying) {
this.stopChatappCronPolling();
if (wasRunning || this.chatappCron.status === "success") {
await this.refreshChatappCronOverview({ silent: true });
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const notFound = errorMessage.includes("chatapp-cron deploy operation not found") || errorMessage.includes("HTTP 404");
if (silent && !notFound) {
return;
}
this.stopChatappCronPolling();
this.chatappCron.deploying = false;
this.chatappCron.status = "failed";
this.chatappCron.error = notFound
? "chatapp-cron 部署任务状态不存在,可能因为服务重启或旧任务已清理"
: errorMessage;
this.setChatappCronMessage(this.explainError(this.chatappCron.error), "warn");
} finally {
this.chatappCron.statusRequestInFlight = false;
}
},
async runChatappCronDeploy() {
if (this.chatappCronIsRunning) {
return;
}
if (!this.chatappCron.overview.enabled) {
this.setChatappCronMessage("chatapp-cron 部署未配置", "warn");
return;
}
const gitRef = String(this.chatappCron.form.gitRef || "").trim()
|| this.chatappCronSettings.defaultGitRef
|| "main";
this.stopChatappCronPolling();
this.chatappCron.deploying = true;
this.chatappCron.error = "";
this.chatappCron.status = "queued";
this.chatappCron.stage = "queued";
this.chatappCron.logs = [];
this.chatappCron.result = null;
this.setChatappCronMessage("chatapp-cron 部署已提交", "neutral");
try {
const { response, payload } = await this.requestJson("/api/chatapp-cron/deploy", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ gitRef }),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.applyChatappCronOperation(payload.operation || {});
this.startChatappCronPolling();
} catch (error) {
this.chatappCron.deploying = false;
this.chatappCron.status = "failed";
this.chatappCron.error = error instanceof Error ? error.message : String(error);
this.setChatappCronMessage(this.explainError(this.chatappCron.error), "warn");
}
},
clearMySqlSchemaState() {
this.mySqlAdmin.schema = {
updatedAt: "",
tableSummary: {},
columns: [],
indexes: [],
identity: {
mode: "all",
columns: [],
},
createTable: "",
};
this.mySqlAdmin.indexForm.columns = [];
},
clearMySqlRowsState() {
this.mySqlAdmin.rows.totalRows = 0;
this.mySqlAdmin.rows.totalPages = 1;
this.mySqlAdmin.rows.columns = [];
this.mySqlAdmin.rows.indexes = [];
this.mySqlAdmin.rows.identity = {
mode: "all",
columns: [],
};
this.mySqlAdmin.rows.items = [];
},
clearMySqlRowEditor() {
this.mySqlAdmin.rowEditor.identityKey = "";
this.mySqlAdmin.rowEditor.identity = null;
this.mySqlAdmin.rowEditor.valuesText = "{}";
},
mySqlFilterNeedsValue(operator) {
return !["is_null", "is_not_null"].includes(String(operator || "").trim().toLowerCase());
},
parseMySqlScalarValue(value) {
const text = String(value ?? "").trim();
if (!text.length) {
return "";
}
if (text === "null") {
return null;
}
if (text === "true") {
return true;
}
if (text === "false") {
return false;
}
if (/^-?\d+(\.\d+)?$/.test(text)) {
return Number(text);
}
try {
return JSON.parse(text);
} catch (_error) {
return text;
}
},
parseMySqlObjectText(text, label = "JSON") {
const raw = String(text || "").trim();
if (!raw) {
return {};
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
throw new Error(`${label} 解析失败`);
}
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
throw new Error(`${label} 需要 JSON 对象`);
}
return parsed;
},
stringifyMySqlJson(value) {
return JSON.stringify(value ?? {}, null, 2);
},
quoteMySqlIdentifier(name) {
return `\`${String(name || "").replaceAll("`", "``")}\``;
},
escapeMySqlString(value) {
return String(value || "").replaceAll("\\", "\\\\").replaceAll("'", "\\'");
},
mySqlRowKey(row) {
return JSON.stringify(row?.identity || {});
},
mySqlCellText(value) {
if (value == null) {
return "NULL";
}
if (typeof value === "object") {
return JSON.stringify(value);
}
return String(value);
},
mySqlCellPreview(value, limit = 88) {
const text = this.mySqlCellText(value).replace(/\s+/g, " ").trim();
if (!text) {
return "-";
}
return text.length <= limit ? text : `${text.slice(0, limit - 1)}`;
},
mySqlReplicationLabel(replication) {
if (!replication || typeof replication !== "object") {
return "-";
}
if (replication.configured) {
return replication.role === "replica" ? "从库" : "主库";
}
return replication.readOnly || replication.superReadOnly ? "只读" : "单节点";
},
mySqlReplicationMeta(replication) {
if (!replication || typeof replication !== "object") {
return "-";
}
const parts = [];
if (replication.configured && replication.sourceHost) {
parts.push(replication.sourceHost);
}
if (replication.secondsBehindSource != null) {
parts.push(`延迟 ${replication.secondsBehindSource}s`);
}
if (replication.readOnly || replication.superReadOnly) {
parts.push("只读");
}
if (!parts.length && replication.state) {
parts.push(replication.state);
}
return parts.join(" / ") || "-";
},
mySqlReplicationTone(replication) {
if (!replication || typeof replication !== "object") {
return "neutral";
}
if (replication.error) {
return "bad";
}
if (replication.configured) {
return replication.ioRunning && replication.sqlRunning ? "ok" : "warn";
}
if (replication.readOnly || replication.superReadOnly) {
return "warn";
}
return "neutral";
},
mySqlRowIdentityText(identity) {
if (!identity || !Array.isArray(identity.columns) || identity.columns.length === 0) {
return "-";
}
return identity.columns.map((column) => {
const rawValue = identity.values?.[column];
return `${column}=${this.mySqlCellPreview(rawValue, 32)}`;
}).join(" / ");
},
isMySqlRowSelected(row) {
return this.mySqlAdmin.rowEditor.identityKey && this.mySqlAdmin.rowEditor.identityKey === this.mySqlRowKey(row);
},
mySqlSortLabel(columnName) {
if (!columnName || this.mySqlAdmin.rows.orderBy !== columnName) {
return "";
}
return this.mySqlAdmin.rows.orderDirection === "asc" ? "ASC" : "DESC";
},
buildMySqlInsertValues(seedValues = null) {
const seeded = seedValues && typeof seedValues === "object" && !Array.isArray(seedValues) ? seedValues : {};
const values = {};
(this.mySqlAdmin.schema.columns || []).forEach((column) => {
const extra = String(column.extra || "").trim().toLowerCase();
if (extra.includes("generated") || extra.includes("auto_increment")) {
return;
}
if (Object.prototype.hasOwnProperty.call(seeded, column.name)) {
values[column.name] = seeded[column.name];
return;
}
if (column.default != null && column.default !== "") {
values[column.name] = column.default;
return;
}
values[column.name] = column.nullable ? null : "";
});
return values;
},
fillMySqlInsertTemplate() {
this.mySqlAdmin.insertForm.valuesText = this.stringifyMySqlJson(this.buildMySqlInsertValues());
this.mySqlAdmin.activeTab = "write";
this.setMySqlAdminMessage("已生成新增模板", "neutral");
},
copyMySqlRowToInsert(row = null) {
try {
const sourceValues = row?.values || (this.mySqlAdmin.rowEditor.valuesText
? this.parseMySqlObjectText(this.mySqlAdmin.rowEditor.valuesText, "编辑行")
: {});
this.mySqlAdmin.insertForm.valuesText = this.stringifyMySqlJson(this.buildMySqlInsertValues(sourceValues));
this.mySqlAdmin.activeTab = "write";
this.setMySqlAdminMessage("已复制为新增模板", "neutral");
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
}
},
async toggleMySqlSort(columnName) {
const actualColumn = String(columnName || "").trim();
if (!actualColumn) {
return;
}
if (this.mySqlAdmin.rows.orderBy === actualColumn) {
this.mySqlAdmin.rows.orderDirection = this.mySqlAdmin.rows.orderDirection === "asc" ? "desc" : "asc";
} else {
this.mySqlAdmin.rows.orderBy = actualColumn;
this.mySqlAdmin.rows.orderDirection = "asc";
}
await this.refreshMySqlRows({ resetPage: true, silent: true });
},
addMySqlFilterRow() {
this.mySqlAdmin.rows.filters.push(createMySqlFilterRow());
},
removeMySqlFilterRow(index) {
const next = [...this.mySqlAdmin.rows.filters];
next.splice(index, 1);
this.mySqlAdmin.rows.filters = next.length > 0 ? next : [createMySqlFilterRow()];
},
buildMySqlFiltersPayload() {
return (this.mySqlAdmin.rows.filters || [])
.filter((item) => String(item.column || "").trim())
.map((item) => {
const payload = {
column: item.column,
operator: item.operator || "eq",
};
if (this.mySqlFilterNeedsValue(item.operator)) {
payload.value = this.parseMySqlScalarValue(item.value);
}
return payload;
});
},
async loadMySqlWorkbench(options = {}) {
await this.refreshMySqlDatabases(options);
},
async refreshMySqlOverview({ silent = false } = {}) {
try {
const { response, payload } = await this.requestJson("/api/mysql/overview");
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.applyMySqlOverviewToPayload(payload);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.applyMySqlOverviewToPayload({
...createMySqlOverviewState(),
updatedAt: new Date().toISOString(),
error: message,
});
if (!silent) {
this.mySqlAdmin.error = message;
}
}
},
async refreshMySqlDatabases({ preserveSelection = true, silent = false } = {}) {
this.mySqlAdmin.loading = true;
if (!silent) {
this.mySqlAdmin.error = "";
}
const previousDatabase = preserveSelection ? this.mySqlAdmin.selectedDatabase : "";
const previousTable = preserveSelection ? this.mySqlAdmin.selectedTable : "";
try {
await this.refreshMySqlOverview({ silent: true });
const { response, payload } = await this.requestJson("/api/mysql/databases");
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.databases = Array.isArray(payload.items) ? payload.items : [];
this.mySqlAdmin.loaded = true;
const nextDatabase = previousDatabase
&& this.mySqlAdmin.databases.some((database) => database.name === previousDatabase)
? previousDatabase
: (payload.selectedDatabase || this.mySqlAdmin.databases[0]?.name || "");
if (!nextDatabase) {
this.mySqlAdmin.selectedDatabase = "";
this.mySqlAdmin.selectedTable = "";
this.mySqlAdmin.tables = [];
this.clearMySqlSchemaState();
this.clearMySqlRowsState();
this.clearMySqlRowEditor();
if (!silent) {
this.setMySqlAdminMessage("当前没有可操作的数据库", "neutral");
}
return;
}
await this.selectMySqlDatabase(nextDatabase, {
preserveTable: preserveSelection,
preferredTable: previousTable,
silent: true,
});
if (!silent) {
this.setMySqlAdminMessage(`已加载 ${this.mySqlAdmin.databases.length} 个数据库`, "neutral");
}
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.loading = false;
}
},
async selectMySqlDatabase(databaseName, { preserveTable = true, preferredTable = "", silent = false } = {}) {
const actualDatabase = String(databaseName || "").trim();
if (!actualDatabase) {
return;
}
this.mySqlAdmin.selectedDatabase = actualDatabase;
await this.refreshMySqlTables({
databaseName: actualDatabase,
preserveTable,
preferredTable,
silent,
});
},
async refreshMySqlTables({
databaseName = this.mySqlAdmin.selectedDatabase,
preserveTable = true,
preferredTable = "",
silent = false,
loadRows = true,
} = {}) {
const actualDatabase = String(databaseName || "").trim();
if (!actualDatabase) {
this.mySqlAdmin.selectedDatabase = "";
this.mySqlAdmin.selectedTable = "";
this.mySqlAdmin.tables = [];
this.clearMySqlSchemaState();
this.clearMySqlRowsState();
this.clearMySqlRowEditor();
return;
}
this.mySqlAdmin.tablesLoading = true;
if (!silent) {
this.mySqlAdmin.error = "";
}
try {
const query = new URLSearchParams({ database: actualDatabase });
const { response, payload } = await this.requestJson(`/api/mysql/tables?${query.toString()}`);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.tables = Array.isArray(payload.items) ? payload.items : [];
this.mySqlAdmin.selectedDatabase = payload.database || actualDatabase;
const currentTable = preserveTable ? (preferredTable || this.mySqlAdmin.selectedTable) : "";
const nextTable = currentTable
&& this.mySqlAdmin.tables.some((table) => table.name === currentTable)
? currentTable
: (payload.selectedTable || this.mySqlAdmin.tables[0]?.name || "");
if (!nextTable) {
this.mySqlAdmin.selectedTable = "";
this.clearMySqlSchemaState();
this.clearMySqlRowsState();
this.clearMySqlRowEditor();
return;
}
if (loadRows) {
await this.selectMySqlTable(this.mySqlAdmin.selectedDatabase, nextTable, { silent: true });
return;
}
this.mySqlAdmin.selectedTable = nextTable;
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.tablesLoading = false;
}
},
async selectMySqlTable(databaseName, tableName, { silent = false } = {}) {
const actualDatabase = String(databaseName || "").trim();
const actualTable = String(tableName || "").trim();
if (!actualDatabase || !actualTable) {
return;
}
this.mySqlAdmin.selectedDatabase = actualDatabase;
this.mySqlAdmin.selectedTable = actualTable;
this.clearMySqlRowEditor();
await this.refreshMySqlSchema({ silent: true });
await this.refreshMySqlRows({ resetPage: true, silent: true });
if (!silent) {
this.setMySqlAdminMessage(`已切换到 ${actualTable}`, "neutral");
}
},
async refreshMySqlSchema({ silent = false } = {}) {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable) {
return;
}
this.mySqlAdmin.schemaLoading = true;
if (!silent) {
this.mySqlAdmin.error = "";
}
try {
const query = new URLSearchParams({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
});
const { response, payload } = await this.requestJson(`/api/mysql/table-schema?${query.toString()}`);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.schema = {
updatedAt: payload.updatedAt || "",
tableSummary: payload.tableSummary || {},
columns: Array.isArray(payload.columns) ? payload.columns : [],
indexes: Array.isArray(payload.indexes) ? payload.indexes : [],
identity: payload.identity || { mode: "all", columns: [] },
createTable: payload.createTable || "",
};
const availableColumns = new Set(this.mySqlAdmin.schema.columns.map((column) => column.name));
this.mySqlAdmin.indexForm.columns = (this.mySqlAdmin.indexForm.columns || [])
.filter((columnName) => availableColumns.has(columnName));
this.mySqlAdmin.rows.filters = (this.mySqlAdmin.rows.filters || [])
.map((item) => (availableColumns.has(item.column) || !item.column ? item : createMySqlFilterRow()));
if (!this.mySqlAdmin.rows.filters.length) {
this.mySqlAdmin.rows.filters = [createMySqlFilterRow()];
}
if (!availableColumns.has(this.mySqlAdmin.rows.orderBy)) {
this.mySqlAdmin.rows.orderBy = payload.identity?.columns?.[0] || this.mySqlAdmin.schema.columns[0]?.name || "";
}
if (!availableColumns.has(this.mySqlAdmin.columnForm.afterColumn)) {
this.mySqlAdmin.columnForm.afterColumn = this.mySqlAdmin.schema.columns.slice(-1)[0]?.name || "";
}
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.schemaLoading = false;
}
},
async refreshMySqlRows({ resetPage = false, silent = false } = {}) {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable) {
return;
}
if (resetPage) {
this.mySqlAdmin.rows.page = 1;
}
this.mySqlAdmin.rowsLoading = true;
if (!silent) {
this.mySqlAdmin.error = "";
}
try {
const { response, payload } = await this.requestJson("/api/mysql/rows/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
page: this.mySqlAdmin.rows.page,
pageSize: this.mySqlAdmin.rows.pageSize,
keyword: this.mySqlAdmin.rows.keyword,
orderBy: this.mySqlAdmin.rows.orderBy,
orderDirection: this.mySqlAdmin.rows.orderDirection,
filters: this.buildMySqlFiltersPayload(),
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.rows.page = Number(payload.page || this.mySqlAdmin.rows.page);
this.mySqlAdmin.rows.pageSize = Number(payload.pageSize || this.mySqlAdmin.rows.pageSize);
this.mySqlAdmin.rows.totalRows = Number(payload.totalRows || 0);
this.mySqlAdmin.rows.totalPages = Number(payload.totalPages || 1);
this.mySqlAdmin.rows.orderBy = payload.orderBy || this.mySqlAdmin.rows.orderBy;
this.mySqlAdmin.rows.orderDirection = payload.orderDirection || this.mySqlAdmin.rows.orderDirection;
this.mySqlAdmin.rows.columns = Array.isArray(payload.columns) ? payload.columns : [];
this.mySqlAdmin.rows.indexes = Array.isArray(payload.indexes) ? payload.indexes : [];
this.mySqlAdmin.rows.identity = payload.identity || { mode: "all", columns: [] };
this.mySqlAdmin.rows.items = Array.isArray(payload.rows) ? payload.rows : [];
if (
this.mySqlAdmin.rowEditor.identityKey
&& !this.mySqlAdmin.rows.items.some((item) => this.mySqlRowKey(item) === this.mySqlAdmin.rowEditor.identityKey)
) {
this.clearMySqlRowEditor();
}
if (!silent) {
this.setMySqlAdminMessage(`已加载 ${this.mySqlAdmin.rows.items.length}`, "neutral");
}
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.rowsLoading = false;
}
},
resetMySqlRowQuery() {
this.mySqlAdmin.rows.keyword = "";
this.mySqlAdmin.rows.page = 1;
this.mySqlAdmin.rows.pageSize = 100;
this.mySqlAdmin.rows.filters = [createMySqlFilterRow()];
this.mySqlAdmin.rows.orderBy = this.mySqlAdmin.schema.identity?.columns?.[0] || this.mySqlAdmin.schema.columns[0]?.name || "";
this.mySqlAdmin.rows.orderDirection = "desc";
},
async changeMySqlRowsPage(nextPage) {
const page = Math.max(1, Math.min(Number(nextPage || 1), this.mySqlAdmin.rows.totalPages || 1));
if (page === this.mySqlAdmin.rows.page) {
return;
}
this.mySqlAdmin.rows.page = page;
await this.refreshMySqlRows({ silent: true });
},
async refreshMySqlCurrent() {
const tasks = [this.refreshMySqlOverview({ silent: true })];
if (this.mySqlAdmin.selectedDatabase) {
tasks.push(this.refreshMySqlTables({
databaseName: this.mySqlAdmin.selectedDatabase,
preserveTable: true,
preferredTable: this.mySqlAdmin.selectedTable,
silent: true,
loadRows: false,
}));
}
if (this.mySqlAdmin.selectedTable) {
tasks.push(this.refreshMySqlSchema({ silent: true }));
if (this.mySqlAdmin.activeTab !== "sql") {
tasks.push(this.refreshMySqlRows({ silent: true }));
}
}
await Promise.allSettled(tasks);
},
loadMySqlRowForEdit(row) {
this.mySqlAdmin.rowEditor.identityKey = this.mySqlRowKey(row);
this.mySqlAdmin.rowEditor.identity = row.identity || null;
this.mySqlAdmin.rowEditor.valuesText = this.stringifyMySqlJson(row.values || {});
this.mySqlAdmin.activeTab = "write";
},
async refreshMySqlAfterMutation({ resetPage = false } = {}) {
await this.refreshMySqlOverview({ silent: true });
if (this.mySqlAdmin.selectedDatabase) {
await this.refreshMySqlTables({
databaseName: this.mySqlAdmin.selectedDatabase,
preserveTable: true,
preferredTable: this.mySqlAdmin.selectedTable,
silent: true,
loadRows: false,
});
}
if (this.mySqlAdmin.selectedTable) {
await this.refreshMySqlSchema({ silent: true });
await this.refreshMySqlRows({ resetPage, silent: true });
}
},
async insertMySqlRow() {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable) {
return;
}
this.mySqlAdmin.insertForm.saving = true;
this.mySqlAdmin.error = "";
try {
const values = this.parseMySqlObjectText(this.mySqlAdmin.insertForm.valuesText, "新增行");
const { response, payload } = await this.requestJson("/api/mysql/rows/insert", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
values,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.insertForm.valuesText = "{}";
this.setMySqlAdminMessage(payload.message || "已插入 1 行", "ok");
await this.refreshMySqlAfterMutation({ resetPage: true });
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.insertForm.saving = false;
}
},
async updateMySqlRow() {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable || !this.mySqlAdmin.rowEditor.identity) {
return;
}
this.mySqlAdmin.rowEditor.saving = true;
this.mySqlAdmin.error = "";
try {
const values = this.parseMySqlObjectText(this.mySqlAdmin.rowEditor.valuesText, "编辑行");
const { response, payload } = await this.requestJson("/api/mysql/rows/update", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
identity: this.mySqlAdmin.rowEditor.identity,
values,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMySqlAdminMessage(payload.message || "已更新 1 行", "ok");
await this.refreshMySqlAfterMutation();
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.rowEditor.saving = false;
}
},
async deleteMySqlRow(row = null) {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable) {
return;
}
const identity = row?.identity || this.mySqlAdmin.rowEditor.identity;
if (!identity) {
return;
}
if (!window.confirm(`删除当前行?\n${this.mySqlRowIdentityText(identity)}`)) {
return;
}
this.mySqlAdmin.rowEditor.saving = true;
this.mySqlAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mysql/rows/delete", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
identity,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
if (this.mySqlRowKey({ identity }) === this.mySqlAdmin.rowEditor.identityKey) {
this.clearMySqlRowEditor();
}
this.setMySqlAdminMessage(payload.message || "已删除 1 行", "ok");
await this.refreshMySqlAfterMutation({ resetPage: true });
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.rowEditor.saving = false;
}
},
async addMySqlColumn() {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable || !this.mySqlAdmin.columnForm.columnName.trim()) {
return;
}
if (this.mySqlAdmin.columnForm.position === "after" && !this.mySqlAdmin.columnForm.afterColumn.trim()) {
this.mySqlAdmin.error = "afterColumn is required";
return;
}
this.mySqlAdmin.columnForm.saving = true;
this.mySqlAdmin.error = "";
try {
const payloadBody = {
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
columnName: this.mySqlAdmin.columnForm.columnName.trim(),
columnType: this.mySqlAdmin.columnForm.columnType.trim(),
nullable: this.mySqlAdmin.columnForm.nullable,
defaultMode: this.mySqlAdmin.columnForm.defaultMode,
defaultValue: this.mySqlAdmin.columnForm.defaultMode === "literal"
? this.parseMySqlScalarValue(this.mySqlAdmin.columnForm.defaultValue)
: "",
comment: this.mySqlAdmin.columnForm.comment,
position: this.mySqlAdmin.columnForm.position,
afterColumn: this.mySqlAdmin.columnForm.position === "after"
? this.mySqlAdmin.columnForm.afterColumn
: "",
};
const { response, payload } = await this.requestJson("/api/mysql/columns/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payloadBody),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.columnForm.columnName = "";
this.mySqlAdmin.columnForm.comment = "";
this.mySqlAdmin.columnForm.defaultValue = "";
this.mySqlAdmin.columnForm.position = "last";
this.setMySqlAdminMessage(payload.message || "列已新增", "ok");
await this.refreshMySqlAfterMutation({ resetPage: true });
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.columnForm.saving = false;
}
},
async dropMySqlColumn(columnName) {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable || !columnName) {
return;
}
if (!window.confirm(`删除列 ${columnName}`)) {
return;
}
this.mySqlAdmin.columnForm.saving = true;
this.mySqlAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mysql/columns/drop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
columnName,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMySqlAdminMessage(payload.message || "列已删除", "ok");
await this.refreshMySqlAfterMutation({ resetPage: true });
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.columnForm.saving = false;
}
},
toggleMySqlIndexColumn(columnName) {
const next = new Set(this.mySqlAdmin.indexForm.columns);
if (next.has(columnName)) {
next.delete(columnName);
} else {
next.add(columnName);
}
this.mySqlAdmin.indexForm.columns = [...next];
},
async addMySqlIndex() {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable) {
return;
}
if (!this.mySqlAdmin.indexForm.indexName.trim() || this.mySqlAdmin.indexForm.columns.length === 0) {
return;
}
this.mySqlAdmin.indexForm.saving = true;
this.mySqlAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mysql/indexes/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
indexName: this.mySqlAdmin.indexForm.indexName.trim(),
indexMode: this.mySqlAdmin.indexForm.indexMode,
columns: this.mySqlAdmin.indexForm.columns,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.indexForm.indexName = "";
this.mySqlAdmin.indexForm.columns = [];
this.setMySqlAdminMessage(payload.message || "索引已新增", "ok");
await this.refreshMySqlAfterMutation();
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.indexForm.saving = false;
}
},
async dropMySqlIndex(indexName) {
if (!this.mySqlAdmin.selectedDatabase || !this.mySqlAdmin.selectedTable || !indexName) {
return;
}
if (!window.confirm(`删除索引 ${indexName}`)) {
return;
}
this.mySqlAdmin.indexForm.saving = true;
this.mySqlAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mysql/indexes/drop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
table: this.mySqlAdmin.selectedTable,
indexName,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMySqlAdminMessage(payload.message || "索引已删除", "ok");
await this.refreshMySqlAfterMutation();
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.indexForm.saving = false;
}
},
applyMySqlSqlShortcut(shortcut) {
if (!shortcut || shortcut.disabled || !shortcut.sql) {
return;
}
this.mySqlAdmin.sqlConsole.text = `${shortcut.sql.trim()};`;
},
async executeMySqlSql() {
const sqlText = String(this.mySqlAdmin.sqlConsole.text || "").trim();
if (!sqlText) {
return;
}
this.mySqlAdmin.sqlConsole.running = true;
this.mySqlAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mysql/sql/execute", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mySqlAdmin.selectedDatabase,
sql: sqlText,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mySqlAdmin.sqlConsole.result = payload;
if (Array.isArray(payload.columns) && payload.columns.length > 0) {
this.setMySqlAdminMessage(`SQL 返回 ${payload.rowCount || 0}`, "neutral");
} else {
this.setMySqlAdminMessage(payload.message || `${String(payload.kind || "SQL").toUpperCase()} 完成`, "ok");
await this.refreshMySqlAfterMutation({ resetPage: false });
}
} catch (error) {
this.mySqlAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mySqlAdmin.sqlConsole.running = false;
}
},
setMongoAdminMessage(message, tone = "neutral") {
this.mongoAdmin.message = message || "";
this.mongoAdmin.messageTone = tone || "neutral";
},
clearMongoAdminCollectionState() {
this.mongoAdmin.collectionDetail = {
updatedAt: "",
stats: {
count: 0,
sizeMb: 0,
storageSizeMb: 0,
avgObjSizeBytes: 0,
indexCount: 0,
totalIndexSizeMb: 0,
capped: false,
wiredTigerUri: "",
},
indexes: [],
};
this.mongoAdmin.query.total = 0;
this.mongoAdmin.query.pageCount = 1;
this.mongoAdmin.query.items = [];
this.mongoAdmin.aggregate.total = 0;
this.mongoAdmin.aggregate.pageCount = 1;
this.mongoAdmin.aggregate.items = [];
this.mongoAdmin.distinct.count = 0;
this.mongoAdmin.distinct.values = [];
this.mongoAdmin.distinct.valuesText = "[]";
this.mongoAdmin.createDocument.ordered = true;
this.mongoAdmin.collectionOps.renameDropTarget = false;
this.clearMongoDocumentEditor();
},
clearMongoDocumentEditor() {
this.mongoAdmin.editor.selectedKey = "";
this.mongoAdmin.editor.idText = "";
this.mongoAdmin.editor.filterText = "{}";
this.mongoAdmin.editor.documentText = "{}";
this.mongoAdmin.editor.upsert = false;
},
syncMongoExplorerState(selectedDatabase = this.mongoAdmin.selectedDatabase, { force = false } = {}) {
const databaseNames = (this.mongoAdmin.databases || []).map((database) => database.name);
if (databaseNames.length === 0) {
this.collapsedMongoDatabases = [];
return;
}
if (force || this.collapsedMongoDatabases.length === 0) {
this.collapsedMongoDatabases = databaseNames.filter((databaseName) => databaseName !== selectedDatabase);
return;
}
this.collapsedMongoDatabases = this.collapsedMongoDatabases.filter((databaseName) => databaseNames.includes(databaseName));
if (selectedDatabase && this.collapsedMongoDatabases.includes(selectedDatabase)) {
this.collapsedMongoDatabases = this.collapsedMongoDatabases.filter((databaseName) => databaseName !== selectedDatabase);
}
},
toggleMongoDatabaseGroup(databaseName) {
const next = new Set(this.collapsedMongoDatabases);
if (next.has(databaseName)) {
next.delete(databaseName);
} else {
next.add(databaseName);
}
this.collapsedMongoDatabases = [...next];
},
isMongoDatabaseCollapsed(databaseName) {
return this.collapsedMongoDatabases.includes(databaseName);
},
isMongoCollectionSelected(databaseName, collectionName) {
return this.mongoAdmin.selectedDatabase === databaseName && this.mongoAdmin.selectedCollection === collectionName;
},
isMongoEmptyFilterText(value) {
const text = String(value || "").trim();
return !text || text === "{}";
},
async refreshMongoAdminOverview({ preserveSelection = true, silent = false } = {}) {
this.mongoAdmin.loading = true;
if (!silent) {
this.mongoAdmin.error = "";
}
const previousDatabase = preserveSelection ? this.mongoAdmin.selectedDatabase : "";
const previousCollection = preserveSelection ? this.mongoAdmin.selectedCollection : "";
const preserveDatabaseOnlyView = preserveSelection && Boolean(previousDatabase) && !previousCollection;
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/overview");
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mongoAdmin.databases = Array.isArray(payload.items) ? payload.items : [];
this.mongoAdmin.loaded = true;
const hasPreviousDatabase = previousDatabase
&& this.mongoAdmin.databases.some((database) => database.name === previousDatabase);
const nextDatabase = hasPreviousDatabase
? previousDatabase
: (payload.selectedDatabase || this.mongoAdmin.databases[0]?.name || "");
const databaseItem = this.mongoAdmin.databases.find((database) => database.name === nextDatabase) || null;
const hasPreviousCollection = previousCollection
&& (databaseItem?.collections || []).some((collection) => collection.name === previousCollection);
const nextCollection = preserveDatabaseOnlyView
? ""
: hasPreviousCollection
? previousCollection
: (databaseItem?.collections || [])[0]?.name || "";
this.mongoAdmin.selectedDatabase = nextDatabase;
this.mongoAdmin.selectedCollection = nextCollection;
this.mongoAdmin.collectionOps.createDatabaseName = nextDatabase || payload.focusedDatabase || "";
this.mongoAdmin.collectionOps.renameName = nextCollection || "";
this.syncMongoExplorerState(nextDatabase, { force: !preserveSelection });
if (!nextCollection) {
this.clearMongoAdminCollectionState();
if (!silent) {
this.setMongoAdminMessage(`已加载 ${this.mongoAdmin.databases.length} 个库`, "neutral");
}
return;
}
if (previousDatabase !== nextDatabase || previousCollection !== nextCollection) {
this.clearMongoAdminCollectionState();
}
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.runMongoQuery({ resetPage: true, silent: true }),
]);
if (!silent) {
this.setMongoAdminMessage(`已加载 ${this.mongoAdmin.databases.length} 个库`, "neutral");
}
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.loading = false;
}
},
async selectMongoDatabase(databaseName, { loadCollection = true } = {}) {
const database = (this.mongoAdmin.databases || []).find((item) => item.name === databaseName);
if (!database) {
return;
}
this.mongoAdmin.selectedDatabase = database.name;
this.mongoAdmin.collectionOps.createDatabaseName = database.name;
this.syncMongoExplorerState(database.name);
if (!loadCollection) {
this.mongoAdmin.selectedCollection = "";
this.mongoAdmin.collectionOps.renameName = "";
this.clearMongoAdminCollectionState();
return;
}
const currentCollectionStillExists = (database.collections || []).some(
(collection) => collection.name === this.mongoAdmin.selectedCollection,
);
const nextCollection = currentCollectionStillExists
? this.mongoAdmin.selectedCollection
: (database.collections || [])[0]?.name || "";
if (!nextCollection) {
this.mongoAdmin.selectedCollection = "";
this.mongoAdmin.collectionOps.renameName = "";
this.clearMongoAdminCollectionState();
return;
}
await this.selectMongoCollection(database.name, nextCollection);
},
async selectMongoCollection(databaseName, collectionName) {
if (!databaseName || !collectionName) {
return;
}
this.mongoAdmin.selectedDatabase = databaseName;
this.mongoAdmin.selectedCollection = collectionName;
this.mongoAdmin.collectionOps.createDatabaseName = databaseName;
this.mongoAdmin.collectionOps.renameName = collectionName;
this.syncMongoExplorerState(databaseName);
this.clearMongoAdminCollectionState();
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.runMongoQuery({ resetPage: true, silent: true }),
]);
},
async refreshMongoCollectionDetail({ silent = false } = {}) {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
this.mongoAdmin.detailLoading = true;
if (!silent) {
this.mongoAdmin.error = "";
}
try {
const query = new URLSearchParams({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
});
const { response, payload } = await this.requestJson(`/api/mongo-admin/collection-detail?${query.toString()}`);
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mongoAdmin.collectionDetail = payload;
this.mongoAdmin.collectionOps.renameName = this.mongoAdmin.selectedCollection;
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.detailLoading = false;
}
},
async refreshMongoAdminCurrent() {
if (!this.mongoAdmin.selectedCollection) {
await this.refreshMongoAdminOverview({ preserveSelection: true });
return;
}
const tasks = [this.refreshMongoCollectionDetail()];
if (this.mongoAdmin.activeTab === "aggregate") {
tasks.push(this.runMongoAggregate({ silent: true }));
} else {
tasks.push(this.runMongoQuery({ silent: true }));
}
await Promise.allSettled(tasks);
},
resetMongoQueryForm() {
this.mongoAdmin.query.filterText = "{}";
this.mongoAdmin.query.sortText = JSON.stringify({ _id: -1 }, null, 2);
this.mongoAdmin.query.projectionText = "{}";
this.mongoAdmin.query.page = 1;
this.mongoAdmin.query.pageSize = 100;
},
async runMongoQuery({ resetPage = false, silent = false } = {}) {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
if (resetPage) {
this.mongoAdmin.query.page = 1;
}
this.mongoAdmin.querying = true;
if (!silent) {
this.mongoAdmin.error = "";
}
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
filter: this.mongoAdmin.query.filterText,
sort: this.mongoAdmin.query.sortText,
projection: this.mongoAdmin.query.projectionText,
page: this.mongoAdmin.query.page,
pageSize: this.mongoAdmin.query.pageSize,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mongoAdmin.query.page = Number(payload.page || this.mongoAdmin.query.page);
this.mongoAdmin.query.pageSize = Number(payload.pageSize || this.mongoAdmin.query.pageSize);
this.mongoAdmin.query.total = Number(payload.total || 0);
this.mongoAdmin.query.pageCount = Number(payload.pageCount || 1);
this.mongoAdmin.query.items = Array.isArray(payload.items) ? payload.items : [];
this.mongoAdmin.query.filterText = payload.filterText || this.mongoAdmin.query.filterText;
this.mongoAdmin.query.sortText = payload.sortText || this.mongoAdmin.query.sortText;
this.mongoAdmin.query.projectionText = payload.projectionText || this.mongoAdmin.query.projectionText;
if (
this.mongoAdmin.editor.selectedKey
&& !this.mongoAdmin.query.items.some((item) => item.key === this.mongoAdmin.editor.selectedKey)
) {
this.clearMongoDocumentEditor();
}
if (!silent) {
this.setMongoAdminMessage(`已加载 ${this.mongoAdmin.query.items.length} 条文档`, "neutral");
}
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.querying = false;
}
},
async changeMongoQueryPage(nextPage) {
const page = Math.max(1, Math.min(Number(nextPage || 1), this.mongoAdmin.query.pageCount || 1));
if (page === this.mongoAdmin.query.page) {
return;
}
this.mongoAdmin.query.page = page;
await this.runMongoQuery({ silent: true });
},
loadMongoDocumentForEdit(item) {
this.mongoAdmin.editor.selectedKey = item.key || "";
this.mongoAdmin.editor.idText = item.idText || "";
this.mongoAdmin.editor.filterText = item.idFilterText || "{}";
this.mongoAdmin.editor.documentText = item.jsonText || "{}";
this.mongoAdmin.editor.upsert = false;
this.mongoAdmin.activeTab = "browse";
},
async saveMongoEditedDocument() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection || !this.mongoAdmin.editor.filterText) {
return;
}
this.mongoAdmin.editor.saving = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/documents/replace", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
filter: this.mongoAdmin.editor.filterText,
document: this.mongoAdmin.editor.documentText,
upsert: this.mongoAdmin.editor.upsert,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage(`已保存 ${payload.modifiedCount || payload.matchedCount || 0}`, "ok");
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.runMongoQuery({ silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.editor.saving = false;
}
},
async deleteMongoDocument(item) {
if (!item?.idFilterText || !this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
if (!window.confirm(`删除 ${item.idText || "当前文档"}`)) {
return;
}
this.mongoAdmin.querying = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/documents/delete", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
filter: item.idFilterText,
many: false,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage("已删除文档", "ok");
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.runMongoQuery({ silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.querying = false;
}
},
async insertMongoDocument() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
this.mongoAdmin.createDocument.saving = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/documents/insert", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
document: this.mongoAdmin.createDocument.documentText,
ordered: this.mongoAdmin.createDocument.ordered,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mongoAdmin.createDocument.documentText = "{}";
this.mongoAdmin.createDocument.ordered = true;
this.setMongoAdminMessage(`已写入 ${payload.insertedCount || 0}`, "ok");
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.runMongoQuery({ resetPage: true, silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.createDocument.saving = false;
}
},
async runMongoUpdateDocuments() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
const allowEmptyFilter = this.mongoAdmin.updateOp.many && this.isMongoEmptyFilterText(this.mongoAdmin.updateOp.filterText);
if (allowEmptyFilter && !window.confirm("当前条件为空,将更新整表文档。继续?")) {
return;
}
this.mongoAdmin.updateOp.running = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/documents/update", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
filter: this.mongoAdmin.updateOp.filterText,
update: this.mongoAdmin.updateOp.updateText,
many: this.mongoAdmin.updateOp.many,
upsert: this.mongoAdmin.updateOp.upsert,
allowEmptyFilter,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage(
`匹配 ${payload.matchedCount || 0} / 修改 ${payload.modifiedCount || 0}`,
"ok",
);
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.runMongoQuery({ silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.updateOp.running = false;
}
},
async runMongoDeleteDocuments() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
const allowEmptyFilter = this.mongoAdmin.deleteOp.many && this.isMongoEmptyFilterText(this.mongoAdmin.deleteOp.filterText);
if (!window.confirm(this.mongoAdmin.deleteOp.many ? "删除当前条件命中的全部文档?" : "删除当前条件命中的一条文档?")) {
return;
}
if (allowEmptyFilter && !window.confirm("当前条件为空,将删除整表文档。继续?")) {
return;
}
this.mongoAdmin.deleteOp.running = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/documents/delete", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
filter: this.mongoAdmin.deleteOp.filterText,
many: this.mongoAdmin.deleteOp.many,
allowEmptyFilter,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage(`已删除 ${payload.deletedCount || 0}`, "ok");
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.runMongoQuery({ resetPage: true, silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.deleteOp.running = false;
}
},
async runMongoAggregate({ resetPage = false, silent = false } = {}) {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
if (resetPage) {
this.mongoAdmin.aggregate.page = 1;
}
this.mongoAdmin.aggregate.running = true;
if (!silent) {
this.mongoAdmin.error = "";
}
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/aggregate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
pipeline: this.mongoAdmin.aggregate.pipelineText,
page: this.mongoAdmin.aggregate.page,
pageSize: this.mongoAdmin.aggregate.pageSize,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mongoAdmin.aggregate.page = Number(payload.page || this.mongoAdmin.aggregate.page);
this.mongoAdmin.aggregate.pageSize = Number(payload.pageSize || this.mongoAdmin.aggregate.pageSize);
this.mongoAdmin.aggregate.total = Number(payload.total || 0);
this.mongoAdmin.aggregate.pageCount = Number(payload.pageCount || 1);
this.mongoAdmin.aggregate.items = Array.isArray(payload.items) ? payload.items : [];
this.mongoAdmin.aggregate.pipelineText = payload.pipelineText || this.mongoAdmin.aggregate.pipelineText;
if (!silent) {
this.setMongoAdminMessage(`聚合返回 ${this.mongoAdmin.aggregate.items.length}`, "neutral");
}
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.aggregate.running = false;
}
},
async changeMongoAggregatePage(nextPage) {
const page = Math.max(1, Math.min(Number(nextPage || 1), this.mongoAdmin.aggregate.pageCount || 1));
if (page === this.mongoAdmin.aggregate.page) {
return;
}
this.mongoAdmin.aggregate.page = page;
await this.runMongoAggregate({ silent: true });
},
async runMongoDistinct() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection || !this.mongoAdmin.distinct.field.trim()) {
return;
}
this.mongoAdmin.distinct.running = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/distinct", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
field: this.mongoAdmin.distinct.field,
filter: this.mongoAdmin.distinct.filterText,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mongoAdmin.distinct.count = Number(payload.count || 0);
this.mongoAdmin.distinct.values = Array.isArray(payload.values) ? payload.values : [];
this.mongoAdmin.distinct.valuesText = payload.valuesText || "[]";
this.setMongoAdminMessage(`去重返回 ${this.mongoAdmin.distinct.count}`, "neutral");
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.distinct.running = false;
}
},
async createMongoIndex() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
this.mongoAdmin.indexForm.saving = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/indexes/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
keys: this.mongoAdmin.indexForm.keysText,
options: this.mongoAdmin.indexForm.optionsText,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage(`已创建索引 ${payload.indexName || ""}`.trim(), "ok");
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.indexForm.saving = false;
}
},
async dropMongoIndex(indexName) {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection || !indexName) {
return;
}
if (!window.confirm(`删除索引 ${indexName}`)) {
return;
}
this.mongoAdmin.indexForm.saving = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/indexes/drop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
indexName,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage(`已删除索引 ${indexName}`, "ok");
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.indexForm.saving = false;
}
},
async dropMongoSecondaryIndexes() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
if (!window.confirm(`删除表 ${this.mongoAdmin.selectedCollection} 的全部二级索引?`)) {
return;
}
this.mongoAdmin.indexForm.dropAllSaving = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/indexes/drop-secondary", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage(`已删除 ${payload.droppedCount || 0} 个二级索引`, "ok");
await Promise.allSettled([
this.refreshMongoCollectionDetail({ silent: true }),
this.refreshMongoAdminOverview({ preserveSelection: true, silent: true }),
]);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.indexForm.dropAllSaving = false;
}
},
async createMongoCollection() {
const databaseName = (this.mongoAdmin.collectionOps.createDatabaseName || this.mongoAdmin.selectedDatabase || "").trim();
const collectionName = (this.mongoAdmin.collectionOps.createName || "").trim();
if (!databaseName || !collectionName) {
return;
}
this.mongoAdmin.collectionOps.running = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/collections/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: databaseName,
collection: collectionName,
options: this.mongoAdmin.collectionOps.createOptionsText,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.mongoAdmin.collectionOps.createName = "";
this.mongoAdmin.collectionOps.createOptionsText = "{}";
this.setMongoAdminMessage(`已创建表 ${collectionName}`, "ok");
await this.refreshMongoAdminOverview({ preserveSelection: false, silent: true });
await this.selectMongoCollection(databaseName, collectionName);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.collectionOps.running = false;
}
},
async renameMongoCollection() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection || !this.mongoAdmin.collectionOps.renameName.trim()) {
return;
}
const targetName = this.mongoAdmin.collectionOps.renameName.trim();
if (!window.confirm(`重命名表 ${this.mongoAdmin.selectedCollection} -> ${targetName}`)) {
return;
}
this.mongoAdmin.collectionOps.running = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/collections/rename", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
targetCollection: targetName,
dropTarget: this.mongoAdmin.collectionOps.renameDropTarget,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage(`已重命名为 ${targetName}`, "ok");
await this.refreshMongoAdminOverview({ preserveSelection: false, silent: true });
await this.selectMongoCollection(this.mongoAdmin.selectedDatabase, targetName);
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.collectionOps.running = false;
this.mongoAdmin.collectionOps.renameDropTarget = false;
}
},
async dropMongoCollection() {
if (!this.mongoAdmin.selectedDatabase || !this.mongoAdmin.selectedCollection) {
return;
}
if (!window.confirm(`删除表 ${this.mongoAdmin.selectedCollection}`)) {
return;
}
this.mongoAdmin.collectionOps.dropRunning = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/collections/drop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
collection: this.mongoAdmin.selectedCollection,
confirmName: this.mongoAdmin.selectedCollection,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage("已删除表", "ok");
await this.refreshMongoAdminOverview({ preserveSelection: false, silent: true });
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.collectionOps.dropRunning = false;
}
},
async dropMongoDatabase() {
if (!this.mongoAdmin.selectedDatabase) {
return;
}
if (!window.confirm(`删除库 ${this.mongoAdmin.selectedDatabase}`)) {
return;
}
this.mongoAdmin.collectionOps.databaseDropRunning = true;
this.mongoAdmin.error = "";
try {
const { response, payload } = await this.requestJson("/api/mongo-admin/databases/drop", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
database: this.mongoAdmin.selectedDatabase,
confirmName: this.mongoAdmin.selectedDatabase,
}),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.setMongoAdminMessage("已删除库", "ok");
await this.refreshMongoAdminOverview({ preserveSelection: false, silent: true });
} catch (error) {
this.mongoAdmin.error = error instanceof Error ? error.message : String(error);
} finally {
this.mongoAdmin.collectionOps.databaseDropRunning = false;
}
},
async refreshRestartTargets() {
this.restartOps.loading = true;
this.restartOps.error = "";
try {
const { response, payload } = await this.requestJson("/api/ops/restart-targets");
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));
this.recordReleaseHistory("restart", "刷新重启列表", `${this.restartOps.items.length}`, "neutral");
} catch (error) {
this.restartOps.error = error instanceof Error ? error.message : String(error);
this.recordReleaseHistory("restart", "刷新重启列表失败", this.explainError(this.restartOps.error), "bad");
} finally {
this.restartOps.loaded = true;
this.restartOps.loading = false;
if (this.activeView === "release" && !this.updateOps.loading) {
this.runReleasePrecheck();
}
}
},
async refreshUpdateTargets() {
this.updateOps.loading = true;
this.updateOps.error = "";
try {
const { response, payload } = await this.requestJson("/api/ops/update-targets");
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;
}
if (!this.updateOps.adminGitRef) {
this.updateOps.adminGitRef = this.updateOps.defaults.adminGitRef || "main";
} else if (this.updateOps.adminGitRef === "main" && this.updateOps.defaults.adminGitRef) {
this.updateOps.adminGitRef = this.updateOps.defaults.adminGitRef;
}
const available = new Set(this.updateOps.items.map((item) => item.service));
this.updateOps.selectedServices = this.updateOps.selectedServices.filter((service) => available.has(service));
this.recordReleaseHistory("update", "刷新服务列表", `${this.updateOps.items.length}`, "neutral");
} catch (error) {
this.updateOps.error = error instanceof Error ? error.message : String(error);
this.recordReleaseHistory("update", "刷新服务列表失败", this.explainError(this.updateOps.error), "bad");
} finally {
this.updateOps.loaded = true;
this.updateOps.loading = false;
if (this.activeView === "release" && !this.restartOps.loading) {
this.runReleasePrecheck();
}
}
},
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(serviceNames = this.updateOps.selectedServices) {
const requestedServices = Array.isArray(serviceNames) ? serviceNames : this.updateOps.selectedServices;
const services = [...new Set((requestedServices || []).map((service) => String(service || "").trim()).filter(Boolean))];
if (services.length === 0) {
return;
}
this.updateOps.selectedServices = services;
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();
this.recordReleaseHistory("update", "提交服务更新", services.join(", "), "warn");
try {
const { response, payload } = await this.requestJson("/api/ops/update-services", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
services,
javaGitRef: this.updateOps.javaGitRef,
goGitRef: this.updateOps.goGitRef,
adminGitRef: this.updateOps.adminGitRef,
imageTag: this.updateOps.imageTag,
skipMavenBuild: this.updateOps.skipMavenBuild,
}),
});
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;
this.recordReleaseHistory("update", "提交服务更新失败", this.explainError(this.updateOps.error), "bad");
} 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, payload } = await this.requestJson("/api/ops/rolling-restart", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ services: this.restartOps.selectedServices }),
});
if (!response.ok) {
throw new Error(payload.error || `HTTP ${response.status}`);
}
this.restartOps.result = payload;
this.restartOps.message = payload.ok
? (payload.message || "滚动重启完成")
: (payload.error || "滚动重启失败");
this.recordReleaseHistory(
"restart",
payload.ok ? "滚动重启完成" : "滚动重启失败",
(payload.steps || []).map((step) => `${step.service}@${step.host}:${step.status}`).join(" | "),
payload.ok ? "ok" : "bad",
);
await this.refresh();
await this.refreshRestartTargets();
} catch (error) {
this.restartOps.error = error instanceof Error ? error.message : String(error);
this.recordReleaseHistory("restart", "滚动重启失败", this.explainError(this.restartOps.error), "bad");
} finally {
this.restartOps.running = false;
}
},
},
mounted() {
this.bootstrap();
},
beforeUnmount() {
if (this.timer) {
window.clearInterval(this.timer);
}
if (this.copiedTokenTimer) {
window.clearTimeout(this.copiedTokenTimer);
}
this.stopUpdatePolling();
this.stopChatappCronPolling();
},
}).mount("#app");