6094 lines
207 KiB
JavaScript
6094 lines
207 KiB
JavaScript
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 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 刷新" },
|
||
];
|
||
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 RELEASE_SECTIONS = [
|
||
{ id: "release-build", label: "构建参数" },
|
||
{ id: "release-targets", label: "更新目标" },
|
||
{ id: "release-log", label: "执行日志" },
|
||
{ id: "release-restart", label: "滚动重启" },
|
||
];
|
||
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,
|
||
};
|
||
}
|
||
|
||
createApp({
|
||
template: APP_TEMPLATE,
|
||
data() {
|
||
const isCommonMode = CURRENT_APP_CONTEXT.mode === "common";
|
||
return {
|
||
activeView: isCommonMode ? "common" : "overview",
|
||
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,
|
||
releaseSections: RELEASE_SECTIONS,
|
||
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: "",
|
||
releaseKindFilter: "all",
|
||
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(),
|
||
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.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";
|
||
}
|
||
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") {
|
||
return this.commonTool === "mysql" ? this.mySqlAdmin.keyword : this.cdnAdmin.keyword;
|
||
}
|
||
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 {
|
||
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") {
|
||
return this.commonTool === "mysql" ? "搜索数据库 / 表" : "搜索 taskId / URL / 状态";
|
||
}
|
||
return "全局搜索";
|
||
},
|
||
globalSearchDisabled() {
|
||
return this.activeView === "config" && this.configTab === "go";
|
||
},
|
||
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
|
||
);
|
||
},
|
||
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)
|
||
));
|
||
},
|
||
commonToolContextLabel() {
|
||
return this.commonTool === "mysql"
|
||
? "MySQL / 数据 / 写入 / 结构 / SQL"
|
||
: "CDN / URL 刷新 / 目录刷新 / 最近记录";
|
||
},
|
||
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;
|
||
},
|
||
releaseTargetRows() {
|
||
const keyword = this.normalizedReleaseKeyword;
|
||
return (this.updateOps.items || [])
|
||
.map((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(),
|
||
};
|
||
})
|
||
.filter((item) => {
|
||
if (this.releaseKindFilter !== "all" && item.kind !== this.releaseKindFilter) {
|
||
return false;
|
||
}
|
||
if (keyword && !item.searchText.includes(keyword)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
})
|
||
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
|
||
},
|
||
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 (this.releaseKindFilter !== "all" && item.kind !== this.releaseKindFilter) {
|
||
return false;
|
||
}
|
||
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.updateStatusLabel(),
|
||
tone: this.updateStatusBadgeClass(),
|
||
},
|
||
{
|
||
label: "失败阶段",
|
||
value: this.updateOps.error ? (this.updateOps.stage || "failed") : "-",
|
||
tone: this.updateOps.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 || "-"}`;
|
||
}
|
||
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(" · ");
|
||
}
|
||
return `任务 ${this.updateOps.operationId || "-"} · ${this.updateStatusLabel()} · ${this.formatDateTime(this.updateOps.finishedAt || this.updateOps.startedAt)}`;
|
||
},
|
||
activeLogViewerBadge() {
|
||
if (this.logViewer.kind === "service") {
|
||
return this.serviceLog.container || "";
|
||
}
|
||
return this.updateOps.operationId || "";
|
||
},
|
||
activeLogViewerSecondaryBadge() {
|
||
if (this.logViewer.kind === "service") {
|
||
return `最近 ${this.serviceLog.previewLines} 条`;
|
||
}
|
||
return this.updateOps.stage ? `阶段 ${this.updateOps.stage}` : "";
|
||
},
|
||
activeLogViewerContent() {
|
||
if (this.logViewer.kind === "service") {
|
||
return this.serviceLog.content;
|
||
}
|
||
return this.updateLogText;
|
||
},
|
||
activeLogViewerError() {
|
||
if (this.logViewer.kind === "service") {
|
||
return this.serviceLog.error;
|
||
}
|
||
return this.updateOps.error ? this.explainError(this.updateOps.error) : "";
|
||
},
|
||
activeLogViewerLoading() {
|
||
if (this.logViewer.kind === "service") {
|
||
return this.serviceLog.loading;
|
||
}
|
||
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;
|
||
}
|
||
return !this.updateOps.operationId || this.updateOps.statusRequestInFlight;
|
||
},
|
||
activeLogViewerDownloadDisabled() {
|
||
if (this.logViewer.kind === "service") {
|
||
return !this.serviceLog.service || !this.serviceLog.host || this.serviceLog.exporting;
|
||
}
|
||
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 (!this.cdnAdmin.loaded && !this.cdnAdmin.loading) {
|
||
await this.refreshCdnOverview({ 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;
|
||
},
|
||
scrollToSection(sectionId) {
|
||
this.$nextTick(() => {
|
||
const node = document.getElementById(sectionId);
|
||
if (node) {
|
||
node.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}
|
||
});
|
||
},
|
||
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) : "-",
|
||
};
|
||
},
|
||
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 checks = [
|
||
{
|
||
label: "更新目标",
|
||
ok: this.updateOps.items.length > 0,
|
||
detail: this.updateOps.items.length > 0 ? `${this.updateOps.items.length} 项可更新服务` : this.explainError(this.updateOps.error) || "暂无更新目标",
|
||
},
|
||
{
|
||
label: "更新选择",
|
||
ok: this.updateOps.selectedServices.length > 0,
|
||
detail: this.updateOps.selectedServices.length > 0 ? this.updateOps.selectedServices.join(", ") : "未选择更新服务",
|
||
},
|
||
{
|
||
label: "滚动重启目标",
|
||
ok: this.restartOps.items.length > 0,
|
||
detail: this.restartOps.items.length > 0 ? `${this.restartOps.items.length} 项可重启服务` : (this.restartOps.error || "暂无重启目标"),
|
||
},
|
||
{
|
||
label: "构建参数",
|
||
ok: (() => {
|
||
const selectedKinds = new Set(
|
||
(this.updateOps.items || [])
|
||
.filter((item) => this.updateOps.selectedServices.includes(item.service))
|
||
.map((item) => item.kind)
|
||
);
|
||
return (
|
||
(!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.label}: ${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());
|
||
},
|
||
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.updateOps.operationId) {
|
||
return;
|
||
}
|
||
await this.fetchUpdateOperationStatus(this.updateOps.operationId);
|
||
this.scrollUpdateLogToBottom();
|
||
},
|
||
async downloadActiveLogViewer() {
|
||
if (this.logViewer.kind === "service") {
|
||
await this.downloadServiceLog();
|
||
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 "未开始";
|
||
},
|
||
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();
|
||
|
||
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 = "";
|
||
|
||
try {
|
||
await Promise.allSettled([
|
||
this.refreshMySqlDatabases({ preserveSelection: true, silent: true }),
|
||
this.refreshCdnOverview({ silent: true }),
|
||
]);
|
||
this.error = this.mySqlAdmin.error || this.cdnAdmin.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";
|
||
},
|
||
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;
|
||
}
|
||
},
|
||
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() {
|
||
if (this.updateOps.selectedServices.length === 0) {
|
||
return;
|
||
}
|
||
|
||
this.stopUpdatePolling();
|
||
this.updateOps.requestEpoch += 1;
|
||
this.updateOps.statusRequestInFlight = false;
|
||
this.updateOps.statusAppliedSeq = 0;
|
||
this.updateOps.running = true;
|
||
this.updateOps.error = "";
|
||
this.updateOps.message = "已提交更新任务,等待开始执行";
|
||
this.updateOps.operationId = "";
|
||
this.updateOps.status = "queued";
|
||
this.updateOps.stage = "queued";
|
||
this.updateOps.startedAt = "";
|
||
this.updateOps.finishedAt = "";
|
||
this.updateOps.logDroppedCount = 0;
|
||
this.updateOps.logs = [];
|
||
this.updateOps.result = null;
|
||
this.persistUpdateOperationId();
|
||
this.recordReleaseHistory("update", "提交服务更新", this.updateOps.selectedServices.join(", "), "warn");
|
||
|
||
try {
|
||
const { response, payload } = await this.requestJson("/api/ops/update-services", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
services: this.updateOps.selectedServices,
|
||
javaGitRef: this.updateOps.javaGitRef,
|
||
goGitRef: this.updateOps.goGitRef,
|
||
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();
|
||
},
|
||
}).mount("#app");
|