mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(xugu): add read-only server monitoring dashboard
This commit is contained in:
@@ -0,0 +1,541 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { Activity, AlertTriangle, Database, Gauge, HardDrive, Loader2, RefreshCcw, Server, Users } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import {
|
||||
XUGU_ACTIVE_SESSION_SQL,
|
||||
XUGU_CLUSTER_NODES_SQL,
|
||||
XUGU_LOCK_MODE_SUMMARY_SQL,
|
||||
XUGU_LOCK_WAITS_SQL,
|
||||
XUGU_MEMORY_STATUS_SQL,
|
||||
XUGU_RUN_INFO_SQL,
|
||||
XUGU_SESSION_SUMMARY_SQL,
|
||||
XUGU_SESSION_STATUS_SUMMARY_SQL,
|
||||
XUGU_TABLESPACE_SUMMARY_SQL,
|
||||
XUGU_TRANSACTION_SUMMARY_SQL,
|
||||
XUGU_VERSION_SQL,
|
||||
connectionSupportsXuguServerDashboard,
|
||||
xuguClusterNodeStateLabel,
|
||||
xuguClusterNodeTypeLabels,
|
||||
xuguClusterNodesFromResult,
|
||||
xuguLockModeSummaryFromResult,
|
||||
xuguMemoryStatusFromResult,
|
||||
xuguRunInfoFromResult,
|
||||
xuguScalarFromResult,
|
||||
xuguSessionSummaryFromResult,
|
||||
xuguSessionStatusSummaryFromResult,
|
||||
xuguTablespaceSummaryFromResult,
|
||||
xuguTransactionSummaryFromResult,
|
||||
xuguVersionFromResult,
|
||||
type XuguClusterNode,
|
||||
type XuguLockModeSummary,
|
||||
type XuguMemoryStatus,
|
||||
type XuguRunInfo,
|
||||
type XuguSessionSummary,
|
||||
type XuguSessionStatusSummary,
|
||||
type XuguTablespaceSummary,
|
||||
type XuguTransactionSummary,
|
||||
} from "@/lib/database/xuguServerStatus";
|
||||
|
||||
const props = defineProps<{ connectionId: string }>();
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const partialErrors = ref<string[]>([]);
|
||||
const version = ref("");
|
||||
const nodes = ref<XuguClusterNode[]>([]);
|
||||
const runInfo = ref<XuguRunInfo[]>([]);
|
||||
const sessions = ref<XuguSessionSummary[]>([]);
|
||||
const transactions = ref<XuguTransactionSummary[]>([]);
|
||||
const memory = ref<XuguMemoryStatus[]>([]);
|
||||
const sessionStatuses = ref<XuguSessionStatusSummary[]>([]);
|
||||
const lockModes = ref<XuguLockModeSummary[]>([]);
|
||||
const tablespaces = ref<XuguTablespaceSummary[]>([]);
|
||||
const lockWaits = ref("");
|
||||
const lockOwners = ref("");
|
||||
const availability = reactive({
|
||||
version: false,
|
||||
nodes: false,
|
||||
runInfo: false,
|
||||
sessions: false,
|
||||
activeSessions: false,
|
||||
transactions: false,
|
||||
lockWaits: false,
|
||||
memory: false,
|
||||
sessionStatuses: false,
|
||||
lockModes: false,
|
||||
tablespaces: false,
|
||||
});
|
||||
const refreshSeconds = ref(5);
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const connection = computed(() => connectionStore.getConfig(props.connectionId));
|
||||
const connectionName = computed(() => connection.value?.name ?? "");
|
||||
const supported = computed(() => connectionSupportsXuguServerDashboard(connection.value));
|
||||
const runInfoByNode = computed(() => new Map(runInfo.value.map((entry) => [entry.nodeId, entry])));
|
||||
const sessionsByNode = computed(() => new Map(sessions.value.map((entry) => [entry.nodeId, entry])));
|
||||
const transactionsByNode = computed(() => new Map(transactions.value.map((entry) => [entry.nodeId, entry])));
|
||||
const onlineNodes = computed<number | null>(() => (availability.nodes ? nodes.value.filter((node) => xuguClusterNodeStateLabel(node.state) === "running").length : null));
|
||||
const totalSessions = computed<number | null>(() => (availability.sessions ? sessions.value.reduce((sum, row) => sum + numeric(row.sessions), 0) : null));
|
||||
const activeSessions = computed<number | null>(() => (availability.activeSessions ? sessions.value.reduce((sum, row) => sum + numeric(row.activeSessions), 0) : null));
|
||||
const activeTransactions = computed<number | null>(() => {
|
||||
if (!availability.transactions && !availability.runInfo) return null;
|
||||
const values = transactions.value.length > 0 ? transactions.value.map((row) => row.activeTransactions) : runInfo.value.map((row) => row.activeTransactions);
|
||||
return values.reduce((sum, value) => sum + numeric(value), 0);
|
||||
});
|
||||
const bufferTotalBytes = computed(() => (availability.memory ? sum(memory.value, "bufferTotalBytes") : null));
|
||||
const bufferFreeBytes = computed(() => (availability.memory ? sum(memory.value, "bufferFreeBytes") : null));
|
||||
const bufferDirtyBytes = computed(() => (availability.memory ? sum(memory.value, "bufferDirtyBytes") : null));
|
||||
const sgaTotalBytes = computed(() => (availability.memory ? sum(memory.value, "sgaTotalBytes") : null));
|
||||
const sgaFreeBytes = computed(() => (availability.memory ? sum(memory.value, "sgaFreeBytes") : null));
|
||||
const swapTotalBytes = computed(() => (availability.memory ? sum(memory.value, "swapTotalBytes") : null));
|
||||
const swapFreeBytes = computed(() => (availability.memory ? sum(memory.value, "swapFreeBytes") : null));
|
||||
const diskReadBytes = computed(() => (availability.runInfo ? sum(runInfo.value, "diskReadBytes") : null));
|
||||
const diskWriteBytes = computed(() => (availability.runInfo ? sum(runInfo.value, "diskWriteBytes") : null));
|
||||
const freeStores = computed(() => (availability.runInfo ? sum(runInfo.value, "freeStores") : null));
|
||||
const transactionSpan = computed<number | null>(() => (availability.runInfo ? runInfo.value.reduce((total, entry) => total + Math.max(0, numeric(entry.maxTransactionId) - numeric(entry.minTransactionId)), 0) : null));
|
||||
const walBacklog = computed<number | null>(() => (availability.runInfo ? runInfo.value.reduce((total, entry) => total + Math.max(0, numeric(entry.xlogWritePosition) - numeric(entry.xlogCheckpointPosition)), 0) : null));
|
||||
const totalDatafiles = computed(() => (availability.tablespaces ? sum(tablespaces.value, "datafiles") : null));
|
||||
const mediaErrors = computed<number | null>(() => (availability.tablespaces ? tablespaces.value.filter((entry) => ["1", "TRUE", "Y"].includes(entry.mediaError.trim().toUpperCase())).length : null));
|
||||
const dataOrTempFreeBytes = computed<number | null>(() => (availability.tablespaces ? tablespaces.value.filter((entry) => /DATA|TEMP/i.test(entry.spaceType)).reduce((total, entry) => total + numeric(entry.freeBytes), 0) : null));
|
||||
|
||||
function numeric(value: string): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function sum<T extends Record<string, string>>(entries: T[], key: keyof T): number {
|
||||
return entries.reduce((total, entry) => total + numeric(entry[key]), 0);
|
||||
}
|
||||
|
||||
function percentage(total: number | null, available: number | null): number {
|
||||
if (total === null || available === null) return 0;
|
||||
if (total <= 0) return 0;
|
||||
return Math.max(0, Math.min(100, ((total - available) / total) * 100));
|
||||
}
|
||||
|
||||
function formatBytes(value: number): string {
|
||||
if (!Number.isFinite(value) || value < 0) return "—";
|
||||
if (value === 0) return "0 B";
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
|
||||
const scaled = value / 1024 ** index;
|
||||
return `${scaled >= 100 || index === 0 ? scaled.toFixed(0) : scaled.toFixed(1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function sessionStatusCount(status: "idle" | "executing"): number | null {
|
||||
if (!availability.sessionStatuses) return null;
|
||||
return sessionStatuses.value
|
||||
.filter((entry) => {
|
||||
const normalized = Math.abs(numeric(entry.status)) % 1000;
|
||||
return status === "idle" ? normalized === 112 : normalized === 114;
|
||||
})
|
||||
.reduce((total, entry) => total + numeric(entry.sessions), 0);
|
||||
}
|
||||
|
||||
function otherSessionCount(): number | null {
|
||||
if (totalSessions.value === null || !availability.sessionStatuses) return null;
|
||||
const idle = sessionStatusCount("idle");
|
||||
const executing = sessionStatusCount("executing");
|
||||
if (idle === null || executing === null) return null;
|
||||
return Math.max(0, totalSessions.value - idle - executing);
|
||||
}
|
||||
|
||||
function lockModeCount(lockMode: string): number | null {
|
||||
if (!availability.lockModes) return null;
|
||||
return lockModes.value.filter((entry) => entry.lockMode.toUpperCase() === lockMode).reduce((total, entry) => total + numeric(entry.locks), 0);
|
||||
}
|
||||
|
||||
function nodeStateClass(node: XuguClusterNode): string {
|
||||
const state = xuguClusterNodeStateLabel(node.state);
|
||||
if (state === "running") return "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
|
||||
if (state === "error" || state === "offline") return "border-destructive/30 bg-destructive/10 text-destructive";
|
||||
return "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300";
|
||||
}
|
||||
|
||||
function nodeStateText(node: XuguClusterNode): string {
|
||||
return t(`xuguServerDashboard.nodeState.${xuguClusterNodeStateLabel(node.state)}`);
|
||||
}
|
||||
|
||||
function nodeTypeText(node: XuguClusterNode): string {
|
||||
const labels = xuguClusterNodeTypeLabels(node.nodeType);
|
||||
return labels.length > 0 ? labels.map((role) => t(`xuguServerDashboard.nodeType.${role}`)).join(" / ") : t("xuguServerDashboard.nodeType.unknown");
|
||||
}
|
||||
|
||||
function valueFor<T extends Record<string, string>>(map: Map<string, T>, nodeId: string, key: keyof T): string {
|
||||
return map.get(nodeId)?.[key] || "—";
|
||||
}
|
||||
|
||||
async function query(sql: string, maxRows: number) {
|
||||
return api.executeQuery(props.connectionId, "", sql, undefined, undefined, { maxRows });
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!supported.value) return;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
partialErrors.value = [];
|
||||
try {
|
||||
await connectionStore.ensureConnected(props.connectionId);
|
||||
const requests = [
|
||||
["version", XUGU_VERSION_SQL, 1],
|
||||
["nodes", XUGU_CLUSTER_NODES_SQL, 500],
|
||||
["runInfo", XUGU_RUN_INFO_SQL, 500],
|
||||
["sessions", XUGU_SESSION_SUMMARY_SQL, 500],
|
||||
["activeSessions", XUGU_ACTIVE_SESSION_SQL, 500],
|
||||
["transactions", XUGU_TRANSACTION_SUMMARY_SQL, 500],
|
||||
["lockWaits", XUGU_LOCK_WAITS_SQL, 1],
|
||||
["memory", XUGU_MEMORY_STATUS_SQL, 500],
|
||||
["sessionStatuses", XUGU_SESSION_STATUS_SUMMARY_SQL, 500],
|
||||
["lockModes", XUGU_LOCK_MODE_SUMMARY_SQL, 500],
|
||||
["tablespaces", XUGU_TABLESPACE_SUMMARY_SQL, 500],
|
||||
] as const;
|
||||
const results = await Promise.allSettled(requests.map(([, sql, maxRows]) => query(sql, maxRows)));
|
||||
const failed = results.filter((result): result is PromiseRejectedResult => result.status === "rejected");
|
||||
partialErrors.value = failed.map((result) => result.reason?.message || String(result.reason));
|
||||
const coreFailed = results.slice(0, 3).every((result) => result.status === "rejected");
|
||||
if (coreFailed) {
|
||||
throw failed[0]?.reason ?? new Error(t("xuguServerDashboard.loadFailed"));
|
||||
}
|
||||
|
||||
const [versionResult, nodeResult, runInfoResult, sessionResult, activeSessionResult, transactionResult, lockResult, memoryResult, sessionStatusResult, lockModeResult, tablespaceResult] = results;
|
||||
availability.version = versionResult.status === "fulfilled";
|
||||
availability.nodes = nodeResult.status === "fulfilled";
|
||||
availability.runInfo = runInfoResult.status === "fulfilled";
|
||||
availability.sessions = sessionResult.status === "fulfilled";
|
||||
availability.activeSessions = activeSessionResult.status === "fulfilled";
|
||||
availability.transactions = transactionResult.status === "fulfilled";
|
||||
availability.lockWaits = lockResult.status === "fulfilled";
|
||||
availability.memory = memoryResult.status === "fulfilled";
|
||||
availability.sessionStatuses = sessionStatusResult.status === "fulfilled";
|
||||
availability.lockModes = lockModeResult.status === "fulfilled";
|
||||
availability.tablespaces = tablespaceResult.status === "fulfilled";
|
||||
if (versionResult.status === "fulfilled") version.value = xuguVersionFromResult(versionResult.value);
|
||||
else version.value = "";
|
||||
if (nodeResult.status === "fulfilled") nodes.value = xuguClusterNodesFromResult(nodeResult.value);
|
||||
else nodes.value = [];
|
||||
if (runInfoResult.status === "fulfilled") runInfo.value = xuguRunInfoFromResult(runInfoResult.value);
|
||||
else runInfo.value = [];
|
||||
const activeByNode = activeSessionResult.status === "fulfilled" ? new Map(xuguSessionSummaryFromResult(activeSessionResult.value).map((row) => [row.nodeId, row])) : new Map();
|
||||
// 活跃会话查询整体失败时不能把缺失显示成硬 0,那会读成"无活跃会话"
|
||||
const activeSessionsUnavailable = activeSessionResult.status !== "fulfilled";
|
||||
sessions.value =
|
||||
sessionResult.status === "fulfilled"
|
||||
? xuguSessionSummaryFromResult(sessionResult.value).map((row) => {
|
||||
const active = activeByNode.get(row.nodeId);
|
||||
return { ...row, activeSessions: active?.activeSessions ?? (activeSessionsUnavailable ? "—" : "0"), oldestStatement: active?.oldestStatement ?? "" };
|
||||
})
|
||||
: [];
|
||||
if (transactionResult.status === "fulfilled") transactions.value = xuguTransactionSummaryFromResult(transactionResult.value);
|
||||
else transactions.value = [];
|
||||
if (lockResult.status === "fulfilled") {
|
||||
lockWaits.value = xuguScalarFromResult(lockResult.value, "LOCK_WAITS");
|
||||
lockOwners.value = xuguScalarFromResult(lockResult.value, "LOCK_OWNERS");
|
||||
} else {
|
||||
lockWaits.value = "";
|
||||
lockOwners.value = "";
|
||||
}
|
||||
if (memoryResult.status === "fulfilled") memory.value = xuguMemoryStatusFromResult(memoryResult.value);
|
||||
else memory.value = [];
|
||||
if (sessionStatusResult.status === "fulfilled") sessionStatuses.value = xuguSessionStatusSummaryFromResult(sessionStatusResult.value);
|
||||
else sessionStatuses.value = [];
|
||||
if (lockModeResult.status === "fulfilled") lockModes.value = xuguLockModeSummaryFromResult(lockModeResult.value);
|
||||
else lockModes.value = [];
|
||||
if (tablespaceResult.status === "fulfilled") tablespaces.value = xuguTablespaceSummaryFromResult(tablespaceResult.value);
|
||||
else tablespaces.value = [];
|
||||
} catch (cause: any) {
|
||||
error.value = cause?.message || String(cause);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh();
|
||||
if (refreshSeconds.value <= 0) return;
|
||||
refreshTimer = setInterval(() => {
|
||||
if (!document.hidden) void load();
|
||||
}, refreshSeconds.value * 1000);
|
||||
}
|
||||
|
||||
function changeRefresh(value: string) {
|
||||
refreshSeconds.value = Number(value);
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
onUnmounted(stopAutoRefresh);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col bg-background">
|
||||
<div class="flex h-11 shrink-0 items-center gap-2 border-b bg-muted/20 px-3">
|
||||
<Gauge class="h-4 w-4 text-primary" />
|
||||
<div class="truncate text-sm font-semibold">{{ t("xuguServerDashboard.title") }}</div>
|
||||
<Badge variant="outline" class="h-5 max-w-56 truncate rounded-md px-1.5 text-[11px]" :title="connectionName">{{ connectionName }}</Badge>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<span class="hidden text-xs text-muted-foreground sm:inline">{{ version || "—" }}</span>
|
||||
<select :value="String(refreshSeconds)" class="h-7 rounded-md border bg-background px-2 text-xs" @change="changeRefresh(($event.target as HTMLSelectElement).value)">
|
||||
<option value="0">{{ t("xuguServerDashboard.off") }}</option>
|
||||
<option value="5">5s</option>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
</select>
|
||||
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" :disabled="loading" @click="load">
|
||||
<Loader2 v-if="loading" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCcw v-else class="h-3.5 w-3.5" />
|
||||
{{ t("grid.refresh") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!supported" class="flex flex-1 items-center justify-center text-sm text-muted-foreground">{{ t("xuguServerDashboard.unsupported") }}</div>
|
||||
<div v-else-if="error" class="flex flex-1 items-center justify-center gap-2 px-6 text-center text-sm text-destructive">
|
||||
<AlertTriangle class="h-4 w-4 shrink-0" />
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else class="min-h-0 flex-1 overflow-auto p-4">
|
||||
<div v-if="partialErrors.length > 0" class="mb-3 flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-300">
|
||||
<AlertTriangle class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||
<span>{{ t("xuguServerDashboard.partialData") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
|
||||
<div class="rounded-lg border bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.version") }}</div>
|
||||
<div class="mt-2 truncate text-sm font-semibold">{{ version || "—" }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-card p-3">
|
||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground"><Server class="h-3.5 w-3.5" />{{ t("xuguServerDashboard.onlineNodes") }}</div>
|
||||
<div class="mt-2 text-xl font-semibold">
|
||||
{{ onlineNodes === null ? "—" : onlineNodes }}<span v-if="onlineNodes !== null" class="ml-1 text-xs font-normal text-muted-foreground">/ {{ nodes.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-card p-3">
|
||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground"><Users class="h-3.5 w-3.5" />{{ t("xuguServerDashboard.sessions") }}</div>
|
||||
<div class="mt-2 text-xl font-semibold">
|
||||
{{ totalSessions === null ? "—" : totalSessions }}<span class="ml-1 text-xs font-normal text-muted-foreground">{{ activeSessions === null ? "—" : t("xuguServerDashboard.activeCount", { count: activeSessions }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-card p-3">
|
||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground"><Activity class="h-3.5 w-3.5" />{{ t("xuguServerDashboard.transactions") }}</div>
|
||||
<div class="mt-2 text-xl font-semibold">{{ activeTransactions === null ? "—" : activeTransactions }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-card p-3">
|
||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground"><Database class="h-3.5 w-3.5" />{{ t("xuguServerDashboard.lockWaits") }}</div>
|
||||
<div class="mt-2 text-xl font-semibold">{{ lockWaits || "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-2">
|
||||
<section class="overflow-hidden rounded-lg border bg-card">
|
||||
<div class="flex items-center justify-between border-b px-3 py-2.5">
|
||||
<div class="flex items-center gap-1.5 text-sm font-semibold"><Database class="h-4 w-4 text-primary" />{{ t("xuguServerDashboard.memoryAndCache") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.memoryHint") }}</div>
|
||||
</div>
|
||||
<div class="space-y-4 p-3">
|
||||
<div>
|
||||
<div class="mb-1.5 flex items-center justify-between text-xs">
|
||||
<span>{{ t("xuguServerDashboard.bufferPool") }}</span
|
||||
><span class="text-muted-foreground">{{ bufferTotalBytes === null || bufferFreeBytes === null ? "—" : `${formatBytes(bufferTotalBytes - bufferFreeBytes)} / ${formatBytes(bufferTotalBytes)}` }}</span>
|
||||
</div>
|
||||
<div class="h-2 overflow-hidden rounded-full bg-muted"><div class="h-full rounded-full bg-primary" :style="{ width: `${percentage(bufferTotalBytes, bufferFreeBytes)}%` }" /></div>
|
||||
<div class="mt-1.5 flex justify-between text-[11px] text-muted-foreground">
|
||||
<span>{{ bufferTotalBytes === null || bufferFreeBytes === null ? "—" : t("xuguServerDashboard.usedPercent", { percent: percentage(bufferTotalBytes, bufferFreeBytes).toFixed(1) }) }}</span
|
||||
><span>{{ bufferDirtyBytes === null ? "—" : t("xuguServerDashboard.dirtyPages", { value: formatBytes(bufferDirtyBytes) }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 flex items-center justify-between text-xs">
|
||||
<span>{{ t("xuguServerDashboard.sga") }}</span
|
||||
><span class="text-muted-foreground">{{ sgaTotalBytes === null || sgaFreeBytes === null ? "—" : `${formatBytes(sgaTotalBytes - sgaFreeBytes)} / ${formatBytes(sgaTotalBytes)}` }}</span>
|
||||
</div>
|
||||
<div class="h-2 overflow-hidden rounded-full bg-muted"><div class="h-full rounded-full bg-sky-500" :style="{ width: `${percentage(sgaTotalBytes, sgaFreeBytes)}%` }" /></div>
|
||||
<div class="mt-1.5 text-[11px] text-muted-foreground">{{ sgaTotalBytes === null || sgaFreeBytes === null ? "—" : t("xuguServerDashboard.usedPercent", { percent: percentage(sgaTotalBytes, sgaFreeBytes).toFixed(1) }) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1.5 flex items-center justify-between text-xs">
|
||||
<span>{{ t("xuguServerDashboard.swap") }}</span
|
||||
><span class="text-muted-foreground">{{ swapTotalBytes === null || swapFreeBytes === null ? "—" : `${formatBytes(swapTotalBytes - swapFreeBytes)} / ${formatBytes(swapTotalBytes)}` }}</span>
|
||||
</div>
|
||||
<div class="h-2 overflow-hidden rounded-full bg-muted"><div class="h-full rounded-full bg-amber-500" :style="{ width: `${percentage(swapTotalBytes, swapFreeBytes)}%` }" /></div>
|
||||
<div class="mt-1.5 text-[11px] text-muted-foreground">{{ swapTotalBytes === null || swapFreeBytes === null ? "—" : t("xuguServerDashboard.usedPercent", { percent: percentage(swapTotalBytes, swapFreeBytes).toFixed(1) }) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="overflow-hidden rounded-lg border bg-card">
|
||||
<div class="flex items-center justify-between border-b px-3 py-2.5">
|
||||
<div class="flex items-center gap-1.5 text-sm font-semibold"><HardDrive class="h-4 w-4 text-primary" />{{ t("xuguServerDashboard.runtimeAndWrite") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.cumulativeHint") }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-px bg-border sm:grid-cols-4">
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.diskRead") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ diskReadBytes === null ? "—" : formatBytes(diskReadBytes) }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.diskWrite") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ diskWriteBytes === null ? "—" : formatBytes(diskWriteBytes) }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.walPending") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ walBacklog === null ? "—" : formatBytes(walBacklog) }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.transactionSpan") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ transactionSpan === null ? "—" : transactionSpan }}</div>
|
||||
</div>
|
||||
<div class="col-span-2 bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.freeStores") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ freeStores === null ? "—" : freeStores }}</div>
|
||||
</div>
|
||||
<div class="col-span-2 bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.datafiles") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ totalDatafiles === null ? "—" : totalDatafiles }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-2">
|
||||
<section class="overflow-hidden rounded-lg border bg-card">
|
||||
<div class="flex items-center justify-between border-b px-3 py-2.5">
|
||||
<div class="flex items-center gap-1.5 text-sm font-semibold"><Users class="h-4 w-4 text-primary" />{{ t("xuguServerDashboard.sessionsAndTransactions") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.activeSessionHint") }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-px bg-border">
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.idleSessions") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ sessionStatusCount("idle") ?? "—" }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.executingSessions") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ sessionStatusCount("executing") ?? "—" }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.otherSessions") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ otherSessionCount() ?? "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3 p-3 text-xs">
|
||||
<div>
|
||||
<div class="text-muted-foreground">{{ t("xuguServerDashboard.oldestStatement") }}</div>
|
||||
<div class="mt-1 truncate font-medium" :title="sessions.find((entry) => entry.oldestStatement)?.oldestStatement">{{ sessions.find((entry) => entry.oldestStatement)?.oldestStatement || "—" }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted-foreground">{{ t("xuguServerDashboard.oldestTransaction") }}</div>
|
||||
<div class="mt-1 truncate font-medium" :title="transactions.find((entry) => entry.oldestTransaction)?.oldestTransaction">{{ transactions.find((entry) => entry.oldestTransaction)?.oldestTransaction || "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="overflow-hidden rounded-lg border bg-card">
|
||||
<div class="flex items-center justify-between border-b px-3 py-2.5">
|
||||
<div class="flex items-center gap-1.5 text-sm font-semibold"><Activity class="h-4 w-4 text-primary" />{{ t("xuguServerDashboard.locksAndCapacity") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.readOnlyHint") }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-px bg-border sm:grid-cols-4">
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.lockOwners") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ lockOwners || "—" }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.lockWaits") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ lockWaits || "—" }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.tablespaces") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold">{{ availability.tablespaces ? tablespaces.length : "—" }}</div>
|
||||
</div>
|
||||
<div class="bg-card p-3">
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.mediaErrors") }}</div>
|
||||
<div class="mt-1.5 text-lg font-semibold" :class="mediaErrors !== null && mediaErrors > 0 ? 'text-destructive' : ''">{{ mediaErrors ?? "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t p-3">
|
||||
<div class="mb-2 flex items-center justify-between text-xs">
|
||||
<span class="text-muted-foreground">{{ t("xuguServerDashboard.lockModes") }}</span
|
||||
><span class="text-muted-foreground">{{ dataOrTempFreeBytes === null ? "—" : t("xuguServerDashboard.dataTempFree", { value: formatBytes(dataOrTempFreeBytes) }) }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-5 gap-2 text-center text-xs">
|
||||
<div v-for="mode in ['S', 'X', 'IS', 'IX', 'SIX']" :key="mode" class="rounded-md bg-muted/50 px-2 py-1.5">
|
||||
<div class="text-muted-foreground">{{ mode }}</div>
|
||||
<div class="mt-0.5 font-semibold">{{ lockModeCount(mode) ?? "—" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-muted-foreground">{{ t("xuguServerDashboard.rowLockLimit") }}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="mt-4 overflow-hidden rounded-lg border bg-card">
|
||||
<div class="flex items-center justify-between border-b px-3 py-2.5">
|
||||
<div class="text-sm font-semibold">{{ t("xuguServerDashboard.nodes") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("xuguServerDashboard.readOnlyHint") }}</div>
|
||||
</div>
|
||||
<div class="overflow-auto">
|
||||
<table class="w-full min-w-[1180px] text-left text-xs">
|
||||
<thead class="bg-muted/40 text-muted-foreground">
|
||||
<tr>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.node") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.type") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.state") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.cpuLoad") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.sessions") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.transactions") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.diskRead") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.diskWrite") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.walPending") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.freeStores") }}</th>
|
||||
<th class="px-3 py-2 font-medium">{{ t("xuguServerDashboard.bootTime") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="node in nodes" :key="node.nodeId" class="border-t hover:bg-muted/30">
|
||||
<td class="px-3 py-2">
|
||||
<div class="font-medium">#{{ node.nodeId }} · {{ node.host || "—" }}</div>
|
||||
<div class="mt-0.5 text-[11px] text-muted-foreground">{{ t("xuguServerDashboard.rack", { rack: node.rackNo || "—" }) }} · {{ t("xuguServerDashboard.stores", { count: node.storeCount || "0" }) }}</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ nodeTypeText(node) }}</td>
|
||||
<td class="px-3 py-2">
|
||||
<Badge variant="outline" class="h-5 rounded-full px-2 text-[10px]" :class="nodeStateClass(node)">{{ nodeStateText(node) }}</Badge>
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ node.cpuLoad || "—" }}</td>
|
||||
<td class="px-3 py-2">
|
||||
{{ valueFor(sessionsByNode, node.nodeId, "sessions") }}<span class="ml-1 text-muted-foreground">/ {{ valueFor(sessionsByNode, node.nodeId, "activeSessions") }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">{{ transactionsByNode.has(node.nodeId) ? valueFor(transactionsByNode, node.nodeId, "activeTransactions") : valueFor(runInfoByNode, node.nodeId, "activeTransactions") }}</td>
|
||||
<td class="px-3 py-2">{{ availability.runInfo ? formatBytes(numeric(valueFor(runInfoByNode, node.nodeId, "diskReadBytes"))) : "—" }}</td>
|
||||
<td class="px-3 py-2">{{ availability.runInfo ? formatBytes(numeric(valueFor(runInfoByNode, node.nodeId, "diskWriteBytes"))) : "—" }}</td>
|
||||
<td class="px-3 py-2">{{ availability.runInfo ? formatBytes(Math.max(0, numeric(valueFor(runInfoByNode, node.nodeId, "xlogWritePosition")) - numeric(valueFor(runInfoByNode, node.nodeId, "xlogCheckpointPosition")))) : "—" }}</td>
|
||||
<td class="px-3 py-2">{{ availability.runInfo ? valueFor(runInfoByNode, node.nodeId, "freeStores") : "—" }}</td>
|
||||
<td class="px-3 py-2 text-muted-foreground">{{ node.bootTime || "—" }}</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && nodes.length === 0">
|
||||
<td colspan="11" class="px-3 py-10 text-center text-muted-foreground">{{ t("xuguServerDashboard.emptyNodes") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -116,6 +116,7 @@ const ProcessListPanel = defineAsyncComponent(() => import("@/components/admin/P
|
||||
const SqlServerActivityTracePanel = defineAsyncComponent(() => import("@/components/admin/SqlServerActivityTracePanel.vue"));
|
||||
const MySqlDashboard = defineAsyncComponent(() => import("@/components/admin/MySqlDashboard.vue"));
|
||||
const PostgresDashboard = defineAsyncComponent(() => import("@/components/admin/PostgresDashboard.vue"));
|
||||
const XuguServerDashboard = defineAsyncComponent(() => import("@/components/admin/XuguServerDashboard.vue"));
|
||||
const DamengJobAdmin = defineAsyncComponent(() => import("@/components/admin/DamengJobAdmin.vue"));
|
||||
const DamengUserAdmin = defineAsyncComponent(() => import("@/components/admin/DamengUserAdmin.vue"));
|
||||
const DamengRoleAdmin = defineAsyncComponent(() => import("@/components/admin/DamengRoleAdmin.vue"));
|
||||
@@ -2559,6 +2560,12 @@ defineExpose({
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeTab.mode === 'xugu-dashboard'">
|
||||
<div class="min-h-0 flex-1">
|
||||
<XuguServerDashboard :key="activeTab.id" :connection-id="activeTab.connectionId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeTab.mode === 'nacos-dashboard'">
|
||||
<div class="min-h-0 flex-1">
|
||||
<NacosDashboard :key="activeTab.id" :connection-id="activeTab.connectionId" />
|
||||
|
||||
@@ -174,6 +174,7 @@ import { authorizationPlanSql, authorizationPlanStatus, buildCreateDatabaseAutho
|
||||
import { connectionSupportsProcessList } from "@/lib/database/processListDrivers";
|
||||
import { connectionSupportsServerDashboard } from "@/lib/database/mysqlServerStatus";
|
||||
import { connectionSupportsServerDashboard as connectionSupportsPgServerDashboard } from "@/lib/database/postgresServerStatus";
|
||||
import { connectionSupportsXuguServerDashboard } from "@/lib/database/xuguServerStatus";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import { sidebarTreeArrowAction } from "@/lib/sidebar/sidebarTreeArrowNavigation";
|
||||
import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty";
|
||||
@@ -1729,6 +1730,8 @@ async function openServerDashboard() {
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
if (currentDatabaseType() === "nacos") {
|
||||
queryStore.openNacosDashboard(node.connectionId);
|
||||
} else if (connectionSupportsXuguServerDashboard(connectionStore.getConfig(node.connectionId))) {
|
||||
queryStore.openXuguDashboard(node.connectionId);
|
||||
} else if (connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId))) {
|
||||
queryStore.openPostgresDashboard(node.connectionId);
|
||||
} else {
|
||||
@@ -5222,7 +5225,10 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
|
||||
if (currentDatabaseType() === "sqlserver") {
|
||||
items.push({ label: t("contextMenu.sqlServerTrace"), action: openSqlServerActivityTrace, icon: Activity });
|
||||
}
|
||||
if (node.connectionId && (currentDatabaseType() === "nacos" || connectionSupportsServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId)))) {
|
||||
if (
|
||||
node.connectionId &&
|
||||
(currentDatabaseType() === "nacos" || connectionSupportsXuguServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId)))
|
||||
) {
|
||||
items.push({ label: t("contextMenu.serverDashboard"), action: openServerDashboard, icon: Gauge });
|
||||
}
|
||||
if (currentDatabaseType() === "dameng") {
|
||||
|
||||
@@ -3619,6 +3619,74 @@ export default {
|
||||
tuplesOutChart: "Tuples out",
|
||||
blockIoChart: "Block I/O",
|
||||
},
|
||||
xuguServerDashboard: {
|
||||
title: "Xugu Server Dashboard",
|
||||
off: "Off",
|
||||
unsupported: "This page only supports XuguDB connections.",
|
||||
loadFailed: "Unable to read Xugu runtime status.",
|
||||
partialData: "Some monitoring views could not be read. The current account may lack system-view privileges; readable data is still shown.",
|
||||
readOnlyHint: "Read-only monitoring; no database state is modified",
|
||||
version: "Database version",
|
||||
onlineNodes: "Online nodes",
|
||||
sessions: "Sessions",
|
||||
activeCount: "{count} active",
|
||||
transactions: "Active transactions",
|
||||
lockWaits: "Lock waits (table-level)",
|
||||
lockOwners: "Held locks",
|
||||
nodes: "Node status",
|
||||
node: "Node",
|
||||
type: "Type",
|
||||
state: "State",
|
||||
cpuLoad: "CPU load (reference)",
|
||||
diskRead: "Disk read",
|
||||
diskWrite: "Disk write",
|
||||
memoryAndCache: "Cache & memory",
|
||||
memoryHint: "From system memory views",
|
||||
bufferPool: "Data buffer pool",
|
||||
sga: "SGA",
|
||||
swap: "Swap",
|
||||
usedPercent: "{percent}% used",
|
||||
dirtyPages: "Dirty {value}",
|
||||
runtimeAndWrite: "Runtime & writes",
|
||||
cumulativeHint: "Cumulative counters; they do not reset on refresh",
|
||||
walPending: "WAL pending checkpoint",
|
||||
transactionSpan: "Transaction ID span (risk signal)",
|
||||
freeStores: "Free stores",
|
||||
datafiles: "Data files",
|
||||
sessionsAndTransactions: "Sessions & transactions",
|
||||
activeSessionHint: "Active sessions include this monitor query",
|
||||
idleSessions: "Idle sessions",
|
||||
executingSessions: "Executing sessions",
|
||||
otherSessions: "Other states",
|
||||
oldestStatement: "Oldest active statement started",
|
||||
oldestTransaction: "Oldest active transaction started",
|
||||
locksAndCapacity: "Locks & capacity",
|
||||
tablespaces: "Tablespaces",
|
||||
mediaErrors: "Media errors",
|
||||
lockModes: "Held lock modes",
|
||||
dataTempFree: "DATA/TEMP free {value}",
|
||||
rowLockLimit: "Lock waits cover table-level locks only; investigate row blocking through long transactions.",
|
||||
bootTime: "Boot time",
|
||||
rack: "Rack {rack}",
|
||||
stores: "{count} stores",
|
||||
emptyNodes: "No node data was returned, or the account cannot access SYS_CLUSTERS.",
|
||||
nodeState: {
|
||||
joining: "Joining",
|
||||
running: "Running",
|
||||
error: "Error",
|
||||
offline: "Offline",
|
||||
unknown: "Unknown",
|
||||
},
|
||||
nodeType: {
|
||||
master: "Master",
|
||||
standby: "Standby",
|
||||
storage: "Storage",
|
||||
query: "Query",
|
||||
worker: "Worker",
|
||||
change: "Change",
|
||||
unknown: "Unknown",
|
||||
},
|
||||
},
|
||||
userAdmin: {
|
||||
title: "Users & Privileges",
|
||||
unsupported: "MySQL-compatible and PostgreSQL-compatible connections are supported. SQL Server, Oracle, and other permission models can be added next.",
|
||||
|
||||
@@ -3606,6 +3606,74 @@ export default withEnglishFallback({
|
||||
tuplesOutChart: "元组读取",
|
||||
blockIoChart: "块 I/O",
|
||||
},
|
||||
xuguServerDashboard: {
|
||||
title: "虚谷服务器仪表盘",
|
||||
off: "关闭",
|
||||
unsupported: "当前页面仅支持虚谷数据库连接。",
|
||||
loadFailed: "无法读取虚谷运行状态。",
|
||||
partialData: "部分监控视图不可读,当前账号可能缺少系统视图权限;已展示可读取的数据。",
|
||||
readOnlyHint: "只读监控,不会修改数据库状态",
|
||||
version: "数据库版本",
|
||||
onlineNodes: "在线节点",
|
||||
sessions: "会话",
|
||||
activeCount: "活跃 {count}",
|
||||
transactions: "活跃事务",
|
||||
lockWaits: "锁等待(表级)",
|
||||
lockOwners: "持有锁",
|
||||
nodes: "节点运行状态",
|
||||
node: "节点",
|
||||
type: "类型",
|
||||
state: "状态",
|
||||
cpuLoad: "CPU 负载(参考)",
|
||||
diskRead: "磁盘读取",
|
||||
diskWrite: "磁盘写入",
|
||||
memoryAndCache: "缓存与内存",
|
||||
memoryHint: "基于系统内存视图",
|
||||
bufferPool: "数据缓冲池",
|
||||
sga: "SGA",
|
||||
swap: "交换区",
|
||||
usedPercent: "已使用 {percent}%",
|
||||
dirtyPages: "脏页 {value}",
|
||||
runtimeAndWrite: "运行与写入",
|
||||
cumulativeHint: "累计计数,刷新不会重置",
|
||||
walPending: "待检查点 WAL",
|
||||
transactionSpan: "事务 ID 跨度(风险信号)",
|
||||
freeStores: "空闲存储段数",
|
||||
datafiles: "数据文件数",
|
||||
sessionsAndTransactions: "会话与事务",
|
||||
activeSessionHint: "活跃会话含本监控查询",
|
||||
idleSessions: "空闲会话",
|
||||
executingSessions: "执行中会话",
|
||||
otherSessions: "其他状态",
|
||||
oldestStatement: "最早执行语句开始时间",
|
||||
oldestTransaction: "最早活跃事务开始时间",
|
||||
locksAndCapacity: "锁与容量",
|
||||
tablespaces: "表空间数",
|
||||
mediaErrors: "介质错误数",
|
||||
lockModes: "当前持有锁模式",
|
||||
dataTempFree: "DATA/TEMP 空闲 {value}",
|
||||
rowLockLimit: "锁等待仅覆盖表级锁;行级阻塞需结合长事务排查。",
|
||||
bootTime: "启动时间",
|
||||
rack: "机架 {rack}",
|
||||
stores: "存储段 {count}",
|
||||
emptyNodes: "未读取到节点信息,或当前账号无权访问 SYS_CLUSTERS。",
|
||||
nodeState: {
|
||||
joining: "加入中",
|
||||
running: "运行中",
|
||||
error: "错误",
|
||||
offline: "离线",
|
||||
unknown: "未知",
|
||||
},
|
||||
nodeType: {
|
||||
master: "主节点",
|
||||
standby: "备节点",
|
||||
storage: "存储节点",
|
||||
query: "查询节点",
|
||||
worker: "工作节点",
|
||||
change: "变更节点",
|
||||
unknown: "未知",
|
||||
},
|
||||
},
|
||||
userAdmin: {
|
||||
title: "用户与权限",
|
||||
unsupported: "当前支持 MySQL 兼容与 PostgreSQL 兼容连接。SQL Server、Oracle 等会按各自权限模型继续扩展。",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
XUGU_ACTIVE_SESSION_SQL,
|
||||
XUGU_CLUSTER_NODES_SQL,
|
||||
XUGU_LOCK_MODE_SUMMARY_SQL,
|
||||
XUGU_LOCK_WAITS_SQL,
|
||||
XUGU_MEMORY_STATUS_SQL,
|
||||
XUGU_RUN_INFO_SQL,
|
||||
XUGU_SESSION_SUMMARY_SQL,
|
||||
XUGU_SESSION_STATUS_SUMMARY_SQL,
|
||||
XUGU_TABLESPACE_SUMMARY_SQL,
|
||||
XUGU_TRANSACTION_SUMMARY_SQL,
|
||||
XUGU_VERSION_SQL,
|
||||
connectionSupportsXuguServerDashboard,
|
||||
xuguClusterNodeStateLabel,
|
||||
xuguClusterNodeTypeLabels,
|
||||
xuguClusterNodesFromResult,
|
||||
xuguLockModeSummaryFromResult,
|
||||
xuguMemoryStatusFromResult,
|
||||
xuguRunInfoFromResult,
|
||||
xuguSessionSummaryFromResult,
|
||||
xuguSessionStatusSummaryFromResult,
|
||||
xuguTablespaceSummaryFromResult,
|
||||
xuguTransactionSummaryFromResult,
|
||||
xuguVersionFromResult,
|
||||
} from "@/lib/database/xuguServerStatus";
|
||||
|
||||
describe("Xugu server status helpers", () => {
|
||||
it("keeps the first-phase catalog queries scoped to Xugu system views", () => {
|
||||
expect(XUGU_VERSION_SQL).toContain("VERSION()");
|
||||
expect(XUGU_CLUSTER_NODES_SQL).toContain("FROM SYS_CLUSTERS");
|
||||
expect(XUGU_RUN_INFO_SQL).toContain("FROM SYS_ALL_RUN_INFO");
|
||||
expect(XUGU_SESSION_SUMMARY_SQL).toContain("FROM SYS_ALL_SESSIONS");
|
||||
expect(XUGU_ACTIVE_SESSION_SQL).toContain("FROM SYS_ALL_THD_SESSION");
|
||||
expect(XUGU_ACTIVE_SESSION_SQL).toContain("JOIN SYS_ALL_SESSIONS");
|
||||
expect(XUGU_ACTIVE_SESSION_SQL).not.toMatch(/WHERE\s+STATE\s*=\s*1/i);
|
||||
expect(XUGU_TRANSACTION_SUMMARY_SQL).toContain("FROM SYS_ALL_TRANS");
|
||||
expect(XUGU_LOCK_WAITS_SQL).toContain("SYS_ALL_LWAITERS");
|
||||
expect(XUGU_LOCK_MODE_SUMMARY_SQL).toContain("SYS_ALL_LOWNERS");
|
||||
expect(XUGU_SESSION_STATUS_SUMMARY_SQL).toContain("FROM SYS_ALL_SESSIONS");
|
||||
expect(XUGU_MEMORY_STATUS_SQL).toContain("SYS_ALL_MEM_STATUS");
|
||||
expect(XUGU_TABLESPACE_SUMMARY_SQL).toContain("SYS_ALL_TABLESPACES");
|
||||
});
|
||||
|
||||
it("recognizes native and JDBC Xugu connections only", () => {
|
||||
expect(connectionSupportsXuguServerDashboard({ id: "x", name: "Xugu", db_type: "xugu" } as any)).toBe(true);
|
||||
expect(connectionSupportsXuguServerDashboard({ id: "pg", name: "Postgres", db_type: "postgres" } as any)).toBe(false);
|
||||
expect(connectionSupportsXuguServerDashboard(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("maps documented node states and roles", () => {
|
||||
expect(xuguClusterNodeStateLabel("2")).toBe("running");
|
||||
expect(xuguClusterNodeStateLabel("4")).toBe("offline");
|
||||
expect(xuguClusterNodeTypeLabels("1")).toEqual(["master"]);
|
||||
expect(xuguClusterNodeTypeLabels("29")).toEqual(["master", "storage", "query", "worker"]);
|
||||
expect(xuguClusterNodeTypeLabels("0")).toEqual([]);
|
||||
expect(xuguClusterNodeStateLabel("999")).toBe("unknown");
|
||||
});
|
||||
|
||||
it("parses status rows by column name instead of column order", () => {
|
||||
const nodes = xuguClusterNodesFromResult({
|
||||
columns: ["NODE_IP", "NODE_ID", "NODE_STATE", "NODE_TYPE", "NODE_PORT", "RACK_NO", "CPU_LOAD", "BOOT_TIME", "STORE_NUM", "MAJOR_NUM"],
|
||||
rows: [["127.0.0.1", 1, 2, 1, 5138, 0, 4, "2026-01-01", 10, 8]],
|
||||
});
|
||||
const runInfo = xuguRunInfoFromResult({
|
||||
columns: ["FREE_STO_N", "NODEID", "ACT_TRANS_NUM", "DISK_R_N", "DISK_R_BYTES", "DISK_W_N", "DISK_W_BYTES", "MAX_TRANS_ID", "MIN_TRANS_ID", "XLOG_WPOS", "XLOG_CKPT"],
|
||||
rows: [[11, 1, 2, 3, 4, 5, 6, 100, 99, 1000, 900]],
|
||||
});
|
||||
const sessions = xuguSessionSummaryFromResult({
|
||||
columns: ["MEMORY_BYTES", "SESSIONS", "NODE_ID", "ACTIVE_SESSIONS", "OLDEST_STATEMENT"],
|
||||
rows: [[1024, 4, 1, 2, "2026-01-01 00:00:00"]],
|
||||
});
|
||||
const transactions = xuguTransactionSummaryFromResult({
|
||||
columns: ["ACTIVE_TRANSACTIONS", "NODE_ID", "OLDEST_TRANSACTION"],
|
||||
rows: [[3, 1, "2026-01-01 00:00:00"]],
|
||||
});
|
||||
const memory = xuguMemoryStatusFromResult({
|
||||
columns: ["SGA_FREE_BYTES", "NODEID", "SGA_TOTAL_BYTES", "BUFFER_FREE_BYTES", "BUFFER_TOTAL_BYTES"],
|
||||
rows: [[20, 1, 100, 40, 200]],
|
||||
});
|
||||
const sessionStatuses = xuguSessionStatusSummaryFromResult({
|
||||
columns: ["SESSIONS", "STATUS", "NODE_ID"],
|
||||
rows: [[3, 112, 1]],
|
||||
});
|
||||
const lockModes = xuguLockModeSummaryFromResult({
|
||||
columns: ["LOCKS", "LOCK_MODE", "NODE_ID"],
|
||||
rows: [[3, "IS", 1]],
|
||||
});
|
||||
const tablespaces = xuguTablespaceSummaryFromResult({
|
||||
columns: ["FREE_BYTES", "TOTAL_BYTES", "MEDIA_ERROR", "DATAFILES", "SPACE_TYPE", "SPACE_NAME", "NODE_ID"],
|
||||
rows: [[10, 100, false, 1, "DATA_SPACE", "DATA1", 1]],
|
||||
});
|
||||
|
||||
expect(nodes[0]).toMatchObject({ nodeId: "1", host: "127.0.0.1", state: "2", majorCount: "8" });
|
||||
expect(runInfo[0]).toMatchObject({ nodeId: "1", activeTransactions: "2", diskReadCount: "3", freeStores: "11", xlogWritePosition: "1000" });
|
||||
expect(sessions[0]).toMatchObject({ nodeId: "1", sessions: "4", activeSessions: "2", memoryBytes: "1024", oldestStatement: "2026-01-01 00:00:00" });
|
||||
expect(transactions[0]).toMatchObject({ nodeId: "1", activeTransactions: "3", oldestTransaction: "2026-01-01 00:00:00" });
|
||||
expect(memory[0]).toMatchObject({ nodeId: "1", sgaTotalBytes: "100", sgaFreeBytes: "20", bufferTotalBytes: "200" });
|
||||
expect(sessionStatuses[0]).toMatchObject({ nodeId: "1", status: "112", sessions: "3" });
|
||||
expect(lockModes[0]).toMatchObject({ nodeId: "1", lockMode: "IS", locks: "3" });
|
||||
expect(tablespaces[0]).toMatchObject({ nodeId: "1", spaceName: "DATA1", spaceType: "DATA_SPACE", totalBytes: "100" });
|
||||
});
|
||||
|
||||
it("returns an empty value for missing or null values", () => {
|
||||
expect(xuguVersionFromResult({ columns: ["VERSION"], rows: [[null]] })).toBe("");
|
||||
expect(xuguVersionFromResult({ columns: ["OTHER"], rows: [["x"]] })).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
import type { ConnectionConfig, QueryResult } from "@/types/database";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
|
||||
export interface XuguClusterNode {
|
||||
nodeId: string;
|
||||
rackNo: string;
|
||||
host: string;
|
||||
port: string;
|
||||
nodeType: string;
|
||||
state: string;
|
||||
cpuLoad: string;
|
||||
bootTime: string;
|
||||
storeCount: string;
|
||||
majorCount: string;
|
||||
}
|
||||
|
||||
export interface XuguRunInfo {
|
||||
nodeId: string;
|
||||
activeTransactions: string;
|
||||
diskReadCount: string;
|
||||
diskReadBytes: string;
|
||||
diskWriteCount: string;
|
||||
diskWriteBytes: string;
|
||||
maxTransactionId: string;
|
||||
minTransactionId: string;
|
||||
xlogWritePosition: string;
|
||||
xlogCheckpointPosition: string;
|
||||
freeStores: string;
|
||||
}
|
||||
|
||||
export interface XuguSessionSummary {
|
||||
nodeId: string;
|
||||
sessions: string;
|
||||
activeSessions: string;
|
||||
memoryBytes: string;
|
||||
oldestStatement: string;
|
||||
}
|
||||
|
||||
export interface XuguTransactionSummary {
|
||||
nodeId: string;
|
||||
activeTransactions: string;
|
||||
oldestTransaction: string;
|
||||
}
|
||||
|
||||
export interface XuguMemoryStatus {
|
||||
nodeId: string;
|
||||
bufferTotalBytes: string;
|
||||
bufferFreeBytes: string;
|
||||
bufferDirtyBytes: string;
|
||||
bufferLruBytes: string;
|
||||
sgaTotalBytes: string;
|
||||
sgaFreeBytes: string;
|
||||
sgaPeakBytes: string;
|
||||
swapTotalBytes: string;
|
||||
swapFreeBytes: string;
|
||||
}
|
||||
|
||||
export interface XuguSessionStatusSummary {
|
||||
nodeId: string;
|
||||
status: string;
|
||||
sessions: string;
|
||||
}
|
||||
|
||||
export interface XuguLockModeSummary {
|
||||
nodeId: string;
|
||||
lockMode: string;
|
||||
locks: string;
|
||||
}
|
||||
|
||||
export interface XuguTablespaceSummary {
|
||||
nodeId: string;
|
||||
spaceName: string;
|
||||
spaceType: string;
|
||||
datafiles: string;
|
||||
mediaError: string;
|
||||
totalBytes: string;
|
||||
freeBytes: string;
|
||||
}
|
||||
|
||||
export const XUGU_VERSION_SQL = "SELECT VERSION() AS VERSION FROM DUAL;";
|
||||
|
||||
export const XUGU_CLUSTER_NODES_SQL = `
|
||||
SELECT
|
||||
NODE_ID,
|
||||
RACK_NO,
|
||||
NODE_IP,
|
||||
NODE_PORT,
|
||||
NODE_TYPE,
|
||||
NODE_STATE,
|
||||
CPU_LOAD,
|
||||
BOOT_TIME,
|
||||
STORE_NUM,
|
||||
MAJOR_NUM
|
||||
FROM SYS_CLUSTERS
|
||||
ORDER BY NODE_ID;`.trim();
|
||||
|
||||
export const XUGU_RUN_INFO_SQL = `
|
||||
SELECT
|
||||
NODEID,
|
||||
ACT_TRANS_NUM,
|
||||
DISK_R_N,
|
||||
DISK_R_BYTES,
|
||||
DISK_W_N,
|
||||
DISK_W_BYTES,
|
||||
MAX_TRANS_ID,
|
||||
MIN_TRANS_ID,
|
||||
XLOG_WPOS,
|
||||
XLOG_CKPT,
|
||||
FREE_STO_N
|
||||
FROM SYS_ALL_RUN_INFO
|
||||
ORDER BY NODEID;`.trim();
|
||||
|
||||
/** Per-node session totals. This is optional because restricted users may not see SYS_ALL_SESSIONS. */
|
||||
export const XUGU_SESSION_SUMMARY_SQL = `
|
||||
SELECT NODEID AS NODE_ID,
|
||||
COUNT(*) AS SESSIONS,
|
||||
SUM(MEM_SIZE) AS MEMORY_BYTES
|
||||
FROM SYS_ALL_SESSIONS
|
||||
GROUP BY NODEID
|
||||
ORDER BY NODEID;`.trim();
|
||||
|
||||
/** The thread-session view is the Xugu-specific definition of an active statement. */
|
||||
export const XUGU_ACTIVE_SESSION_SQL = `
|
||||
SELECT T.NODEID AS NODE_ID,
|
||||
COUNT(*) AS ACTIVE_SESSIONS,
|
||||
MIN(S.CMD_START_T) AS OLDEST_STATEMENT
|
||||
FROM SYS_ALL_THD_SESSION T
|
||||
JOIN SYS_ALL_SESSIONS S
|
||||
ON T.NODEID = S.NODEID AND T.SESSION_ID = S.SESSION_ID
|
||||
GROUP BY T.NODEID
|
||||
ORDER BY T.NODEID;`.trim();
|
||||
|
||||
export const XUGU_TRANSACTION_SUMMARY_SQL = `
|
||||
SELECT NODEID AS NODE_ID,
|
||||
COUNT(*) AS ACTIVE_TRANSACTIONS,
|
||||
MIN(START_T) AS OLDEST_TRANSACTION
|
||||
FROM SYS_ALL_TRANS
|
||||
GROUP BY NODEID
|
||||
ORDER BY NODEID;`.trim();
|
||||
|
||||
/** Only table-level lock waiters are exposed by SYS_ALL_LWAITERS. */
|
||||
export const XUGU_LOCK_WAITS_SQL = `
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM SYS_ALL_LWAITERS) AS LOCK_WAITS,
|
||||
(SELECT COUNT(*) FROM SYS_ALL_LOWNERS) AS LOCK_OWNERS
|
||||
FROM DUAL;`.trim();
|
||||
|
||||
export const XUGU_LOCK_MODE_SUMMARY_SQL = `
|
||||
SELECT NODEID AS NODE_ID,
|
||||
LOCK_LEVEL AS LOCK_MODE,
|
||||
COUNT(*) AS LOCKS
|
||||
FROM SYS_ALL_LOWNERS
|
||||
GROUP BY NODEID, LOCK_LEVEL
|
||||
ORDER BY NODEID, LOCK_LEVEL;`.trim();
|
||||
|
||||
export const XUGU_SESSION_STATUS_SUMMARY_SQL = `
|
||||
SELECT NODEID AS NODE_ID,
|
||||
STATUS,
|
||||
COUNT(*) AS SESSIONS
|
||||
FROM SYS_ALL_SESSIONS
|
||||
GROUP BY NODEID, STATUS
|
||||
ORDER BY NODEID, STATUS;`.trim();
|
||||
|
||||
export const XUGU_MEMORY_STATUS_SQL = `
|
||||
SELECT
|
||||
NODEID,
|
||||
BUFF_SIZE * TOTAL_BUFF_NUM AS BUFFER_TOTAL_BYTES,
|
||||
BUFF_SIZE * FREE_BUFF_NUM AS BUFFER_FREE_BYTES,
|
||||
BUFF_SIZE * DIRTY_BUFF_NUM AS BUFFER_DIRTY_BYTES,
|
||||
BUFF_SIZE * LRU_BUFF_NUM AS BUFFER_LRU_BYTES,
|
||||
SGA_BLK_SIZE * TOTAL_SGA_MEM AS SGA_TOTAL_BYTES,
|
||||
SGA_BLK_SIZE * FREE_SGA_MEM AS SGA_FREE_BYTES,
|
||||
SGA_BLK_SIZE * PEAK_SGA_MEM AS SGA_PEAK_BYTES,
|
||||
SWAP_BLK_SIZE * TOTAL_SWAP_MEM AS SWAP_TOTAL_BYTES,
|
||||
SWAP_BLK_SIZE * FREE_SWAP_MEM AS SWAP_FREE_BYTES
|
||||
FROM SYS_ALL_MEM_STATUS
|
||||
ORDER BY NODEID;`.trim();
|
||||
|
||||
export const XUGU_TABLESPACE_SUMMARY_SQL = `
|
||||
SELECT
|
||||
T.NODEID AS NODE_ID,
|
||||
T.SPACE_NAME,
|
||||
T.SPACE_TYPE,
|
||||
T.DATAFILE_NUM AS DATAFILES,
|
||||
T.MEDIA_ERROR,
|
||||
T.TOTAL_CHUNK_NUM * C.CHUNK_SIZE AS TOTAL_BYTES,
|
||||
T.FREE_CHUNK_NUM * C.CHUNK_SIZE AS FREE_BYTES
|
||||
FROM SYS_ALL_TABLESPACES T, SYS_CTL_VARS C
|
||||
ORDER BY T.NODEID, T.SPACE_NAME;`.trim();
|
||||
|
||||
export function connectionSupportsXuguServerDashboard(connection: ConnectionConfig | undefined): boolean {
|
||||
return !!connection && effectiveDatabaseTypeForConnection(connection) === "xugu";
|
||||
}
|
||||
|
||||
export function xuguClusterNodeStateLabel(state: string): "joining" | "running" | "error" | "offline" | "unknown" {
|
||||
switch (Number(state)) {
|
||||
case 1:
|
||||
return "joining";
|
||||
case 2:
|
||||
return "running";
|
||||
case 3:
|
||||
return "error";
|
||||
case 4:
|
||||
return "offline";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export type XuguClusterNodeRole = "master" | "standby" | "storage" | "query" | "worker" | "change";
|
||||
|
||||
export function xuguClusterNodeTypeLabels(type: string): XuguClusterNodeRole[] {
|
||||
const value = Number(type);
|
||||
if (!Number.isFinite(value) || value <= 0) return [];
|
||||
const roles: Array<[number, XuguClusterNodeRole]> = [
|
||||
[1, "master"],
|
||||
[2, "standby"],
|
||||
[4, "storage"],
|
||||
[8, "query"],
|
||||
[16, "worker"],
|
||||
[32, "change"],
|
||||
];
|
||||
return roles.filter(([bit]) => (value & bit) === bit).map(([, role]) => role);
|
||||
}
|
||||
|
||||
export function xuguClusterNodesFromResult(result: QueryResult): XuguClusterNode[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODE_ID"),
|
||||
rackNo: valueAt(result, row, "RACK_NO"),
|
||||
host: valueAt(result, row, "NODE_IP"),
|
||||
port: valueAt(result, row, "NODE_PORT"),
|
||||
nodeType: valueAt(result, row, "NODE_TYPE"),
|
||||
state: valueAt(result, row, "NODE_STATE"),
|
||||
cpuLoad: valueAt(result, row, "CPU_LOAD"),
|
||||
bootTime: valueAt(result, row, "BOOT_TIME"),
|
||||
storeCount: valueAt(result, row, "STORE_NUM"),
|
||||
majorCount: valueAt(result, row, "MAJOR_NUM"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguRunInfoFromResult(result: QueryResult): XuguRunInfo[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODEID"),
|
||||
activeTransactions: valueAt(result, row, "ACT_TRANS_NUM"),
|
||||
diskReadCount: valueAt(result, row, "DISK_R_N"),
|
||||
diskReadBytes: valueAt(result, row, "DISK_R_BYTES"),
|
||||
diskWriteCount: valueAt(result, row, "DISK_W_N"),
|
||||
diskWriteBytes: valueAt(result, row, "DISK_W_BYTES"),
|
||||
maxTransactionId: valueAt(result, row, "MAX_TRANS_ID"),
|
||||
minTransactionId: valueAt(result, row, "MIN_TRANS_ID"),
|
||||
xlogWritePosition: valueAt(result, row, "XLOG_WPOS"),
|
||||
xlogCheckpointPosition: valueAt(result, row, "XLOG_CKPT"),
|
||||
freeStores: valueAt(result, row, "FREE_STO_N"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguSessionSummaryFromResult(result: QueryResult): XuguSessionSummary[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODE_ID"),
|
||||
sessions: valueAt(result, row, "SESSIONS"),
|
||||
activeSessions: valueAt(result, row, "ACTIVE_SESSIONS"),
|
||||
memoryBytes: valueAt(result, row, "MEMORY_BYTES"),
|
||||
oldestStatement: valueAt(result, row, "OLDEST_STATEMENT"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguTransactionSummaryFromResult(result: QueryResult): XuguTransactionSummary[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODE_ID"),
|
||||
activeTransactions: valueAt(result, row, "ACTIVE_TRANSACTIONS"),
|
||||
oldestTransaction: valueAt(result, row, "OLDEST_TRANSACTION"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguMemoryStatusFromResult(result: QueryResult): XuguMemoryStatus[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODEID"),
|
||||
bufferTotalBytes: valueAt(result, row, "BUFFER_TOTAL_BYTES"),
|
||||
bufferFreeBytes: valueAt(result, row, "BUFFER_FREE_BYTES"),
|
||||
bufferDirtyBytes: valueAt(result, row, "BUFFER_DIRTY_BYTES"),
|
||||
bufferLruBytes: valueAt(result, row, "BUFFER_LRU_BYTES"),
|
||||
sgaTotalBytes: valueAt(result, row, "SGA_TOTAL_BYTES"),
|
||||
sgaFreeBytes: valueAt(result, row, "SGA_FREE_BYTES"),
|
||||
sgaPeakBytes: valueAt(result, row, "SGA_PEAK_BYTES"),
|
||||
swapTotalBytes: valueAt(result, row, "SWAP_TOTAL_BYTES"),
|
||||
swapFreeBytes: valueAt(result, row, "SWAP_FREE_BYTES"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguSessionStatusSummaryFromResult(result: QueryResult): XuguSessionStatusSummary[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODE_ID"),
|
||||
status: valueAt(result, row, "STATUS"),
|
||||
sessions: valueAt(result, row, "SESSIONS"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguLockModeSummaryFromResult(result: QueryResult): XuguLockModeSummary[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODE_ID"),
|
||||
lockMode: valueAt(result, row, "LOCK_MODE"),
|
||||
locks: valueAt(result, row, "LOCKS"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguTablespaceSummaryFromResult(result: QueryResult): XuguTablespaceSummary[] {
|
||||
return result.rows.map((row) => ({
|
||||
nodeId: valueAt(result, row, "NODE_ID"),
|
||||
spaceName: valueAt(result, row, "SPACE_NAME"),
|
||||
spaceType: valueAt(result, row, "SPACE_TYPE"),
|
||||
datafiles: valueAt(result, row, "DATAFILES"),
|
||||
mediaError: valueAt(result, row, "MEDIA_ERROR"),
|
||||
totalBytes: valueAt(result, row, "TOTAL_BYTES"),
|
||||
freeBytes: valueAt(result, row, "FREE_BYTES"),
|
||||
}));
|
||||
}
|
||||
|
||||
export function xuguScalarFromResult(result: QueryResult, column: string): string {
|
||||
return result.rows.length > 0 ? valueAt(result, result.rows[0], column) : "";
|
||||
}
|
||||
|
||||
export function xuguVersionFromResult(result: QueryResult): string {
|
||||
return xuguScalarFromResult(result, "VERSION");
|
||||
}
|
||||
|
||||
function valueAt(result: QueryResult, row: unknown[], column: string): string {
|
||||
const index = result.columns.findIndex((candidate) => candidate.toUpperCase() === column.toUpperCase());
|
||||
return index < 0 || row[index] == null ? "" : String(row[index]);
|
||||
}
|
||||
@@ -2786,6 +2786,29 @@ export const useQueryStore = defineStore("query", () => {
|
||||
return registerOpenTab(tab);
|
||||
}
|
||||
|
||||
function openXuguDashboard(connectionId: string) {
|
||||
const existing = tabs.value.find((tab) => tab.mode === "xugu-dashboard" && tab.connectionId === connectionId);
|
||||
if (existing) {
|
||||
switchTab(existing.id);
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
const conn = useConnectionStore().getConfig(connectionId);
|
||||
const id = uuid();
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title: conn?.name ? `${conn.name} - ${t("xuguServerDashboard.title")}` : t("xuguServerDashboard.title"),
|
||||
connectionId,
|
||||
database: conn?.database || "",
|
||||
sql: "",
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "xugu-dashboard",
|
||||
};
|
||||
return registerOpenTab(tab);
|
||||
}
|
||||
|
||||
function openNacosDashboard(connectionId: string) {
|
||||
const existing = tabs.value.find((tab) => tab.mode === "nacos-dashboard" && tab.connectionId === connectionId);
|
||||
if (existing) {
|
||||
@@ -7731,6 +7754,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
openSqlServerActivityTrace,
|
||||
openMysqlDashboard,
|
||||
openPostgresDashboard,
|
||||
openXuguDashboard,
|
||||
openNacosDashboard,
|
||||
openDamengUsers,
|
||||
openDamengRoles,
|
||||
|
||||
@@ -1263,6 +1263,7 @@ export interface QueryTab {
|
||||
| "sqlserver-trace"
|
||||
| "mysql-dashboard"
|
||||
| "postgres-dashboard"
|
||||
| "xugu-dashboard"
|
||||
| "dolt-version-control";
|
||||
/** Ephemeral navigation intent; it is consumed by HBaseBrowser and is not persisted. */
|
||||
hbaseCreateTableOnOpen?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user