feat(nacos): add Nacos dashboard metrics

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-27 12:16:39 +08:00
1 parent 98bf3d1dfd
commit f9c2fb27d3
41 files changed
+3268 -36

No files matched your search

@@ -13,7 +13,7 @@ use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent
interface Series {
name: string;
data: number[];
data: Array<number | null>;
color?: string;
}
@@ -56,6 +56,7 @@ const chartOption = computed(() => {
name: s.name,
type: "line",
data: s.data,
connectNulls: false,
smooth: true,
showSymbol: false,
lineStyle: s.color ? { color: s.color, width: 2 } : { width: 2 },
@@ -17,7 +17,7 @@ import { Switch } from "@/components/ui/switch";
import type { ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo, DatabaseType, HttpTunnelConfig, IdentifierCase, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshConfigHostEntry, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb";
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
@@ -57,7 +57,7 @@ import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses";
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
import { driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressForOperation, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { normalizeNacosEndpoint } from "@/lib/nacos/nacosAdmin";
import { nacosMetricsCandidates, normalizeNacosEndpoint, normalizeNacosMetricsUrl } from "@/lib/nacos/nacosAdmin";
import {
ArrowLeft,
ArrowDown,
@@ -604,6 +604,8 @@ const nacosAuthKind = ref<NacosAuthKind>("none");
const nacosUsername = ref("nacos");
const nacosPassword = ref("");
const nacosTlsSkipVerify = ref(false);
const nacosMetricsMode = ref<NacosMetricsMode>("auto");
const nacosMetricsUrl = ref("");
const nacosPageSize = ref(20);
const nacosPrimaryAddressLabel = computed(() => {
if (nacosImplementation.value === "rnacos") return t("connection.nacosPrimaryAddressRNacos");
@@ -612,8 +614,7 @@ const nacosPrimaryAddressLabel = computed(() => {
return t("connection.nacosPrimaryAddressAuto");
});
const nacosPrimaryAddressPlaceholder = computed(() => {
if (nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2") return "http://127.0.0.1:8848/nacos";
return "http://127.0.0.1:8080";
return "http://127.0.0.1:8848/nacos";
});
const nacosNormalizedPreview = computed(() => {
if (!nacosServerAddr.value.trim()) return "";
@@ -630,7 +631,7 @@ const nacosNormalizedPreview = computed(() => {
});
const nacosEffectiveContextPath = computed(() => {
if (!nacosServerAddr.value.trim()) {
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2" ? "/nacos" : "/";
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : "/nacos";
}
try {
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
@@ -650,6 +651,28 @@ const nacosContextPathInput = computed({
nacosContextPath.value = value;
},
});
const nacosMetricsAutoPreview = computed(() => {
if (!nacosNormalizedPreview.value) return "";
try {
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
implementation: nacosImplementation.value,
versionMode: nacosVersionMode.value,
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
});
return nacosMetricsCandidates(normalized.serverAddr, normalized.contextPath, nacosImplementation.value).join(" · ");
} catch {
return "";
}
});
const nacosMetricsUrlError = computed(() => {
if (nacosMetricsMode.value !== "custom") return "";
try {
normalizeNacosMetricsUrl(nacosMetricsUrl.value);
return "";
} catch {
return t("connection.nacosMetricsUrlInvalid");
}
});
function resetNacosContextPathCustomization() {
nacosContextPathCustomized.value = false;
@@ -1090,6 +1113,8 @@ function resetNacosFields(config?: Partial<NacosAdminConfig>) {
nacosConsoleUsername.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.username : "";
nacosConsolePassword.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.password : "";
nacosTlsSkipVerify.value = !!config?.tlsSkipVerify;
nacosMetricsMode.value = config?.metricsMode || "auto";
nacosMetricsUrl.value = config?.metricsUrl || "";
nacosPageSize.value = Number(config?.pageSize) > 0 ? Number(config?.pageSize) : 20;
const auth = (config?.auth || { kind: "none" }) as NacosAuthConfig;
nacosAuthKind.value = auth.kind || "none";
@@ -1298,6 +1323,14 @@ function buildNacosAdminConfig(): NacosAdminConfig {
throw new Error(t("connection.nacosRNacosConsoleUrlRequired"));
}
let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined;
let metricsUrl: string | undefined;
if (nacosMetricsMode.value === "custom") {
try {
metricsUrl = normalizeNacosMetricsUrl(nacosMetricsUrl.value);
} catch {
throw new Error(t("connection.nacosMetricsUrlInvalid"));
}
}
if (rnacosConsoleConfigured) {
if (nacosConsoleAuthKind.value === "inherit") {
if (nacosAuthKind.value !== "usernamePassword") throw new Error(t("connection.nacosConsoleAuthSeparateRequired"));
@@ -1321,6 +1354,8 @@ function buildNacosAdminConfig(): NacosAdminConfig {
rnacosConsoleAuth,
auth: buildNacosAuth(),
tlsSkipVerify: nacosTlsSkipVerify.value || undefined,
metricsMode: nacosMetricsMode.value,
metricsUrl,
pageSize: Number(nacosPageSize.value) > 0 ? Number(nacosPageSize.value) : 20,
};
}
@@ -5312,6 +5347,30 @@ function openExternalUrl(url: string) {
<Label :class="connectionLabelClass">{{ t("connection.nacosNamespace") }}</Label>
<Input v-model="nacosNamespace" class="col-span-3" placeholder="public" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.nacosMetrics") }}</Label>
<Select v-model="nacosMetricsMode">
<SelectTrigger class="col-span-3 h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">{{ t("connection.nacosMetricsAuto") }}</SelectItem>
<SelectItem value="disabled">{{ t("connection.nacosMetricsDisabled") }}</SelectItem>
<SelectItem value="custom">{{ t("connection.nacosMetricsCustom") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="nacosMetricsMode === 'auto' && nacosMetricsAutoPreview" class="grid grid-cols-4 items-start gap-4">
<span />
<p class="col-span-3 m-0 break-all text-xs leading-5 text-muted-foreground">{{ t("connection.nacosMetricsAutoHint", { addresses: nacosMetricsAutoPreview }) }}</p>
</div>
<div v-if="nacosMetricsMode === 'custom'" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelClass">{{ t("connection.nacosMetricsUrl") }}</Label>
<div class="col-span-3">
<Input v-model="nacosMetricsUrl" :aria-invalid="!!nacosMetricsUrlError" :class="{ 'border-destructive focus-visible:ring-destructive': nacosMetricsUrlError }" placeholder="http://127.0.0.1:8818/nacos/actuator/prometheus" />
<p v-if="nacosMetricsUrlError" class="mt-1 text-xs text-destructive">{{ nacosMetricsUrlError }}</p>
</div>
</div>
<template v-if="nacosImplementation === 'rnacos'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.nacosConfigurationHistory") }}</Label>
@@ -5,9 +5,6 @@ const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url),
describe("DataGrid native clipboard regions", () => {
it("keeps table info text selection out of grid copy shortcuts", () => {
const drawerTag = dataGridSource.match(/<div\s+v-if="showTableInfo"[^>]*>/)?.[0];
expect(drawerTag).toBeDefined();
expect(drawerTag).toContain("data-native-clipboard");
expect(dataGridSource).toMatch(/<div\b(?=[^>]*\bv-if="showTableInfo")(?=[^>]*\bdata-native-clipboard)[^>]*>/);
});
});
@@ -480,7 +480,7 @@ function tabMenuIcon(tab: QueryTab) {
if (tab.mode === "structure") return PencilRuler;
if (tab.mode === "dameng-jobs") return CalendarClock;
if (tab.mode === "processlist") return Activity;
if (tab.mode === "mysql-dashboard" || tab.mode === "postgres-dashboard") return Gauge;
if (tab.mode === "mysql-dashboard" || tab.mode === "postgres-dashboard" || tab.mode === "nacos-dashboard") return Gauge;
return Code2;
}
@@ -623,7 +623,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
<Activity v-else-if="tab.mode === 'processlist'" class="h-3.5 w-3.5" />
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard'" class="h-3.5 w-3.5" />
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />
</span>
<input
@@ -816,7 +816,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
<Activity v-else-if="tab.mode === 'processlist'" class="h-3.5 w-3.5" />
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard'" class="h-3.5 w-3.5" />
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />
</span>
<input
@@ -52,6 +52,7 @@ const VectorBrowser = defineAsyncComponent(() => import("@/components/vector/Vec
const ElasticsearchJsonResponsePanel = defineAsyncComponent(() => import("@/components/common/ElasticsearchJsonResponsePanel.vue"));
const MqAdminConsole = defineAsyncComponent(() => import("@/components/mq/MqAdminConsole.vue"));
const NacosAdminConsole = defineAsyncComponent(() => import("@/components/nacos/NacosAdminConsole.vue"));
const NacosDashboard = defineAsyncComponent(() => import("@/components/nacos/NacosDashboard.vue"));
const ObjectBrowser = defineAsyncComponent(() => import("@/components/objects/ObjectBrowser.vue"));
const TableStructureEditor = defineAsyncComponent(() => import("@/components/structure/TableStructureEditor.vue"));
const DatabaseUserAdmin = defineAsyncComponent(() => import("@/components/admin/DatabaseUserAdmin.vue"));
@@ -1687,6 +1688,12 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
</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" />
</div>
</template>
<template v-else-if="activeTab.mode === 'dameng-jobs' && activeConnection">
<DamengJobAdmin :key="activeTab.id" :connection="activeConnection" />
</template>
@@ -0,0 +1,467 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { Activity, Boxes, Braces, Cpu, Gauge, HardDrive, Layers3, Loader2, Network, RefreshCw, Server, Users } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import MetricCard from "@/components/common/MetricCard.vue";
import MetricLineChart from "@/components/chart/MetricLineChart.vue";
import * as api from "@/lib/backend/api";
import { formatBytes, formatNumber, formatRate } from "@/lib/database/serverMetrics";
import { useConnectionStore } from "@/stores/connectionStore";
import {
appendDashboardSample,
averageDurationMsSeries,
counterRateSeries,
dashboardMetric,
dashboardNamespaceLabel,
dashboardSeries,
errorRateSeries,
formatDashboardPercent,
gaugeSeries,
isHealthyNacosNode,
ratioPercent,
type NacosDashboardSample,
type NullableMetric,
} from "@/lib/nacos/nacosDashboard";
const props = defineProps<{
connectionId: string;
namespace?: string;
}>();
const { t } = useI18n();
const connectionStore = useConnectionStore();
const loading = ref(false);
const fetching = ref(false);
const error = ref("");
const samples = ref<NacosDashboardSample[]>([]);
const autoRefreshInterval = ref(10);
let refreshTimer: ReturnType<typeof setInterval> | null = null;
let activeRequestKey = "";
let latestRequestId = 0;
const latest = computed(() => samples.value[samples.value.length - 1]);
const snapshot = computed(() => latest.value?.snapshot);
const metrics = computed(() => snapshot.value?.metrics);
const prometheus = computed(() => snapshot.value?.prometheus);
const namespaceLabel = computed(() => dashboardNamespaceLabel(snapshot.value?.namespace, props.namespace));
const healthyNodes = computed(() => snapshot.value?.nodes.filter(isHealthyNacosNode).length ?? 0);
const chartLabels = computed(() => samples.value.map((sample) => formatClock(sample.at)));
const lastUpdated = computed(() => (latest.value ? formatClock(latest.value.at) : "—"));
type ChartSeries = { name: string; data: NullableMetric[]; color?: string };
function hasSeries(series: readonly ChartSeries[]): boolean {
return series.some((item) => item.data.some((value) => value !== null));
}
const countSeries = computed(() => {
const series = [
{
name: t("nacos.dashboardServices"),
data: samples.value.map((sample) => sample.snapshot.serviceCount ?? dashboardMetric(sample, "serviceCount")),
color: "#3b82f6",
},
];
if (samples.value.some((sample) => typeof sample.snapshot.metrics?.instanceCount === "number")) {
series.push({ name: t("nacos.dashboardInstances"), data: dashboardSeries(samples.value, "instanceCount"), color: "#22c55e" });
}
if (samples.value.some((sample) => typeof sample.snapshot.metrics?.clientCount === "number")) {
series.push({ name: t("nacos.dashboardClients"), data: dashboardSeries(samples.value, "clientCount"), color: "#8b5cf6" });
}
return series;
});
const resourceSeries = computed(() => [
{
name: t("nacos.dashboardCpu"),
data: samples.value.map((sample) => ratioPercent(sample.snapshot.prometheus?.resource.cpuRatio ?? sample.snapshot.metrics?.cpu)),
color: "#f59e0b",
},
{
name: t("nacos.dashboardMemory"),
data: samples.value.map((sample) => ratioPercent(sample.snapshot.prometheus?.resource.memoryRatio ?? sample.snapshot.metrics?.mem)),
color: "#ef4444",
},
]);
const hasResourceMetrics = computed(() => hasSeries(resourceSeries.value));
const trafficSeries = computed<ChartSeries[]>(() => [
{
name: t("nacos.dashboardHttpQps"),
data: counterRateSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.httpRequestsTotal),
color: "#3b82f6",
},
{
name: t("nacos.dashboardGrpcQps"),
data: counterRateSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.grpcRequestsTotal),
color: "#8b5cf6",
},
]);
const latencySeries = computed<ChartSeries[]>(() => [
{
name: t("nacos.dashboardHttpAverage"),
data: averageDurationMsSeries(
samples.value,
(sample) => sample.snapshot.prometheus?.traffic.httpDurationSecondsTotal,
(sample) => sample.snapshot.prometheus?.traffic.httpDurationCount,
),
color: "#3b82f6",
},
{
name: t("nacos.dashboardHttpP50"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.httpP50Ms),
color: "#0ea5e9",
},
{
name: t("nacos.dashboardHttpP95"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.httpP95Ms),
color: "#06b6d4",
},
{
name: t("nacos.dashboardHttpP99"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.httpP99Ms),
color: "#14b8a6",
},
{
name: t("nacos.dashboardGrpcAverage"),
data: averageDurationMsSeries(
samples.value,
(sample) => sample.snapshot.prometheus?.traffic.grpcDurationSecondsTotal,
(sample) => sample.snapshot.prometheus?.traffic.grpcDurationCount,
),
color: "#8b5cf6",
},
{
name: t("nacos.dashboardGrpcP50"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.grpcP50Ms),
color: "#a855f7",
},
{
name: t("nacos.dashboardGrpcP95"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.grpcP95Ms),
color: "#d946ef",
},
{
name: t("nacos.dashboardGrpcP99"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.grpcP99Ms),
color: "#ec4899",
},
]);
const errorRateChartSeries = computed<ChartSeries[]>(() => [
{
name: t("nacos.dashboardHttpErrors"),
data: errorRateSeries(
samples.value,
(sample) => sample.snapshot.prometheus?.traffic.httpErrorsTotal,
(sample) => sample.snapshot.prometheus?.traffic.httpRequestsTotal,
),
color: "#ef4444",
},
{
name: t("nacos.dashboardGrpcErrors"),
data: errorRateSeries(
samples.value,
(sample) => sample.snapshot.prometheus?.traffic.grpcErrorsTotal,
(sample) => sample.snapshot.prometheus?.traffic.grpcRequestsTotal,
),
color: "#f97316",
},
]);
const executorSeries = computed<ChartSeries[]>(() => [
{
name: t("nacos.dashboardExecutorPool"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.executorPoolSize),
color: "#3b82f6",
},
{
name: t("nacos.dashboardExecutorActive"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.executorActiveCount),
color: "#22c55e",
},
{
name: t("nacos.dashboardExecutorQueue"),
data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.traffic.executorQueuedTasks),
color: "#ef4444",
},
]);
const configRateSeries = computed<ChartSeries[]>(() => [
{
name: t("nacos.dashboardConfigGets"),
data: counterRateSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.getConfigTotal),
color: "#3b82f6",
},
{
name: t("nacos.dashboardConfigPublishes"),
data: counterRateSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.publishTotal),
color: "#22c55e",
},
]);
const configStateSeries = computed<ChartSeries[]>(() => [
{ name: t("nacos.dashboardLongPolling"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.longPolling), color: "#8b5cf6" },
{ name: t("nacos.dashboardListenerClients"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.listenerClients), color: "#06b6d4" },
{ name: t("nacos.dashboardListenerKeys"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.listenerKeys), color: "#3b82f6" },
{ name: t("nacos.dashboardNotifyTasks"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.notifyTasks), color: "#f59e0b" },
{ name: t("nacos.dashboardNotifyClientTasks"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.notifyClientTasks), color: "#ef4444" },
{ name: t("nacos.dashboardDumpTasks"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.config.dumpTasks), color: "#64748b" },
]);
const namingStateSeries = computed<ChartSeries[]>(() => [
{ name: t("nacos.dashboardServices"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.serviceCount), color: "#3b82f6" },
{ name: t("nacos.dashboardInstances"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.instanceCount), color: "#22c55e" },
{ name: t("nacos.dashboardSubscribers"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.subscriberCount), color: "#8b5cf6" },
{ name: t("nacos.dashboardClients"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.connectionCount), color: "#06b6d4" },
]);
const pushRateSeries = computed<ChartSeries[]>(() => [
{ name: t("nacos.dashboardPushes"), data: counterRateSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.totalPush), color: "#3b82f6" },
{ name: t("nacos.dashboardFailedPushes"), data: counterRateSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.failedPush), color: "#ef4444" },
{ name: t("nacos.dashboardEmptyPushes"), data: counterRateSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.emptyPush), color: "#f59e0b" },
]);
const pushDetailSeries = computed<ChartSeries[]>(() => [
{ name: t("nacos.dashboardPushAverage"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.avgPushCostMs), color: "#3b82f6" },
{ name: t("nacos.dashboardPushMaximum"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.maxPushCostMs), color: "#8b5cf6" },
{ name: t("nacos.dashboardPushPending"), data: gaugeSeries(samples.value, (sample) => sample.snapshot.prometheus?.naming.pushPendingTasks), color: "#ef4444" },
]);
const hasTraffic = computed(() => hasSeries(trafficSeries.value));
const hasLatency = computed(() => hasSeries(latencySeries.value));
const hasReliability = computed(() => hasSeries(errorRateChartSeries.value) || hasSeries(executorSeries.value));
const hasConfigPrometheus = computed(() => hasSeries(configRateSeries.value) || hasSeries(configStateSeries.value));
const hasNamingPrometheus = computed(() => hasSeries(namingStateSeries.value) || hasSeries(pushRateSeries.value) || hasSeries(pushDetailSeries.value));
function formatClock(at: number): string {
const date = new Date(at);
const pad = (value: number) => String(value).padStart(2, "0");
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function optionalCount(value: number | undefined): string {
return typeof value === "number" && Number.isFinite(value) ? formatNumber(value) : "—";
}
function optionalMetric(value: number | undefined): string {
return typeof value === "number" && Number.isFinite(value) ? formatRate(value) : "—";
}
function optionalBytes(value: number | undefined): string {
return typeof value === "number" && Number.isFinite(value) ? formatBytes(value) : "—";
}
function nodeLastRefresh(value: string | undefined): string {
if (!value) return "—";
const timestamp = Number(value);
if (Number.isFinite(timestamp) && timestamp > 1_000_000_000_000) return new Date(timestamp).toLocaleString();
return value;
}
function nodeState(node: { alive?: boolean; state?: string }): string {
if (node.state?.trim()) return node.state;
if (node.alive === true) return t("nacos.healthy");
if (node.alive === false) return t("nacos.unhealthy");
return "—";
}
async function fetchSnapshot(options: { silent?: boolean } = {}) {
const connectionId = props.connectionId;
const namespace = props.namespace || undefined;
const requestKey = `${connectionId}\u0000${namespace ?? ""}`;
if (fetching.value && activeRequestKey === requestKey) return;
const requestId = ++latestRequestId;
activeRequestKey = requestKey;
fetching.value = true;
if (!options.silent) loading.value = true;
error.value = "";
try {
await connectionStore.ensureConnected(connectionId);
const value = await api.nacosGetDashboard(connectionId, { namespace });
if (requestId !== latestRequestId) return;
samples.value = appendDashboardSample(samples.value, { at: Date.now(), snapshot: value });
} catch (cause: unknown) {
if (requestId !== latestRequestId) return;
error.value = cause instanceof Error ? cause.message : String(cause);
} finally {
if (requestId === latestRequestId) {
loading.value = false;
fetching.value = false;
}
}
}
function stopAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
function startAutoRefresh() {
stopAutoRefresh();
if (autoRefreshInterval.value <= 0) return;
refreshTimer = setInterval(() => {
if (document.hidden) return;
void fetchSnapshot({ silent: true });
}, autoRefreshInterval.value * 1000);
}
function onIntervalChange(value: unknown) {
autoRefreshInterval.value = Number(value);
startAutoRefresh();
}
watch(
() => [props.connectionId, props.namespace] as const,
async () => {
samples.value = [];
await fetchSnapshot();
},
);
onMounted(async () => {
await fetchSnapshot();
startAutoRefresh();
});
onUnmounted(() => {
latestRequestId += 1;
stopAutoRefresh();
});
</script>
<template>
<div class="flex h-full min-h-0 flex-col">
<div class="flex h-10 shrink-0 items-center gap-2 border-b px-3">
<Gauge class="h-4 w-4 text-primary" />
<span class="text-xs font-medium">{{ t("nacos.dashboardTitle") }}</span>
<Badge variant="outline" class="h-5 rounded-md px-1.5 text-[11px]">{{ namespaceLabel }}</Badge>
<Badge v-if="metrics?.status" :variant="metrics.status.toUpperCase() === 'UP' ? 'secondary' : 'destructive'" class="h-5 rounded-md px-1.5 text-[11px]">
{{ metrics.status }}
</Badge>
<Badge variant="outline" class="h-5 max-w-48 truncate rounded-md px-1.5 text-[11px]" :title="prometheus?.source.endpoint">
{{ prometheus ? "OpenAPI + Prometheus" : "OpenAPI" }}
</Badge>
<span class="ml-auto text-[11px] text-muted-foreground">{{ t("nacos.dashboardUpdatedAt", { time: lastUpdated }) }}</span>
<span class="text-xs text-muted-foreground">{{ t("nacos.dashboardAutoRefresh") }}</span>
<Select :model-value="String(autoRefreshInterval)" @update:model-value="onIntervalChange">
<SelectTrigger class="h-7 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">{{ t("nacos.dashboardOff") }}</SelectItem>
<SelectItem value="5">5s</SelectItem>
<SelectItem value="10">10s</SelectItem>
<SelectItem value="30">30s</SelectItem>
<SelectItem value="60">60s</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm" class="h-7 gap-1.5 px-2 text-xs" :disabled="loading" @click="fetchSnapshot()">
<Loader2 v-if="loading" class="h-3.5 w-3.5 animate-spin" />
<RefreshCw v-else class="h-3.5 w-3.5" />
{{ t("nacos.refresh") }}
</Button>
</div>
<div v-if="error" class="shrink-0 border-b bg-destructive/10 px-3 py-2 text-xs text-destructive">{{ error }}</div>
<div class="min-h-0 flex-1 overflow-y-auto">
<div class="flex flex-col gap-3 p-3">
<div class="grid shrink-0 grid-cols-2 gap-3 sm:grid-cols-4">
<MetricCard :label="t('nacos.dashboardNamespaces')" :value="optionalCount(snapshot?.namespaceCount)" :icon="Layers3" />
<MetricCard :label="t('nacos.dashboardConfigs')" :value="optionalCount(snapshot?.configCount)" :sub="namespaceLabel" :icon="Braces" />
<MetricCard :label="t('nacos.dashboardServices')" :value="optionalCount(snapshot?.serviceCount)" :sub="namespaceLabel" :icon="Boxes" />
<MetricCard :label="t('nacos.dashboardInstances')" :value="optionalCount(metrics?.instanceCount)" :icon="Network" />
<MetricCard :label="t('nacos.dashboardClients')" :value="optionalCount(metrics?.clientCount)" :sub="t('nacos.dashboardConnections', { count: optionalCount(metrics?.connectionBasedClientCount) })" :icon="Users" />
<MetricCard :label="t('nacos.dashboardNodes')" :value="snapshot?.nodes.length ? `${healthyNodes} / ${snapshot.nodes.length}` : '—'" :icon="Server" />
<MetricCard :label="t('nacos.dashboardCpu')" :value="formatDashboardPercent(metrics?.cpu)" :icon="Cpu" />
<MetricCard :label="t('nacos.dashboardMemory')" :value="formatDashboardPercent(metrics?.mem)" :sub="t('nacos.dashboardLoad', { value: optionalMetric(metrics?.load) })" :icon="HardDrive" />
</div>
<div v-if="prometheus" class="grid shrink-0 grid-cols-2 gap-3 sm:grid-cols-4">
<MetricCard v-if="prometheus.resource.memoryUsedBytes !== undefined" :label="t('nacos.dashboardMemoryUsed')" :value="optionalBytes(prometheus.resource.memoryUsedBytes)" :sub="t('nacos.dashboardMemoryMax', { value: optionalBytes(prometheus.resource.memoryMaxBytes) })" :icon="HardDrive" />
<MetricCard v-if="prometheus.resource.rssBytes !== undefined" :label="t('nacos.dashboardRss')" :value="optionalBytes(prometheus.resource.rssBytes)" :sub="t('nacos.dashboardVms', { value: optionalBytes(prometheus.resource.vmsBytes) })" :icon="HardDrive" />
<MetricCard v-if="prometheus.resource.jvmDaemonThreads !== undefined" :label="t('nacos.dashboardJvmThreads')" :value="optionalCount(prometheus.resource.jvmDaemonThreads)" :icon="Activity" />
<MetricCard v-if="prometheus.resource.gcPauseCount !== undefined" :label="t('nacos.dashboardGcPauses')" :value="optionalCount(prometheus.resource.gcPauseCount)" :icon="Activity" />
</div>
<div class="grid shrink-0 grid-cols-1 gap-3 xl:grid-cols-2">
<MetricLineChart :class="{ 'xl:col-span-2': !hasResourceMetrics }" :title="t('nacos.dashboardCountsChart')" :labels="chartLabels" :series="countSeries" :value-formatter="formatNumber" />
<MetricLineChart v-if="hasResourceMetrics" :title="t('nacos.dashboardResourcesChart')" :labels="chartLabels" :series="resourceSeries" :value-formatter="(value) => `${value.toFixed(1)}%`" />
</div>
<template v-if="hasTraffic || hasLatency">
<div class="text-xs font-semibold text-foreground">{{ t("nacos.dashboardTrafficSection") }}</div>
<div class="grid shrink-0 grid-cols-1 gap-3 xl:grid-cols-2">
<MetricLineChart v-if="hasTraffic" :title="t('nacos.dashboardRequestRateChart')" :labels="chartLabels" :series="trafficSeries" :value-formatter="formatRate" />
<MetricLineChart v-if="hasLatency" :class="{ 'xl:col-span-2': !hasTraffic }" :title="t('nacos.dashboardLatencyChart')" :labels="chartLabels" :series="latencySeries" :value-formatter="(value) => `${formatRate(value)} ms`" />
</div>
</template>
<template v-if="hasReliability">
<div class="text-xs font-semibold text-foreground">{{ t("nacos.dashboardReliabilitySection") }}</div>
<div class="grid shrink-0 grid-cols-1 gap-3 xl:grid-cols-2">
<MetricLineChart v-if="hasSeries(errorRateChartSeries)" :title="t('nacos.dashboardErrorRateChart')" :labels="chartLabels" :series="errorRateChartSeries" :value-formatter="(value) => `${value.toFixed(2)}%`" />
<MetricLineChart v-if="hasSeries(executorSeries)" :class="{ 'xl:col-span-2': !hasSeries(errorRateChartSeries) }" :title="t('nacos.dashboardExecutorChart')" :labels="chartLabels" :series="executorSeries" :value-formatter="formatNumber" />
</div>
</template>
<template v-if="hasConfigPrometheus">
<div class="text-xs font-semibold text-foreground">{{ t("nacos.dashboardConfigSection") }}</div>
<div class="grid shrink-0 grid-cols-1 gap-3 xl:grid-cols-2">
<MetricLineChart v-if="hasSeries(configRateSeries)" :title="t('nacos.dashboardConfigRateChart')" :labels="chartLabels" :series="configRateSeries" :value-formatter="formatRate" />
<MetricLineChart v-if="hasSeries(configStateSeries)" :class="{ 'xl:col-span-2': !hasSeries(configRateSeries) }" :title="t('nacos.dashboardConfigStateChart')" :labels="chartLabels" :series="configStateSeries" :value-formatter="formatNumber" />
</div>
</template>
<template v-if="hasNamingPrometheus">
<div class="text-xs font-semibold text-foreground">{{ t("nacos.dashboardNamingSection") }}</div>
<div class="grid shrink-0 grid-cols-1 gap-3 xl:grid-cols-2">
<MetricLineChart v-if="hasSeries(namingStateSeries)" :title="t('nacos.dashboardNamingStateChart')" :labels="chartLabels" :series="namingStateSeries" :value-formatter="formatNumber" />
<MetricLineChart v-if="hasSeries(pushRateSeries)" :class="{ 'xl:col-span-2': !hasSeries(namingStateSeries) }" :title="t('nacos.dashboardPushRateChart')" :labels="chartLabels" :series="pushRateSeries" :value-formatter="formatRate" />
<MetricLineChart v-if="hasSeries(pushDetailSeries)" class="xl:col-span-2" :title="t('nacos.dashboardPushDetailChart')" :labels="chartLabels" :series="pushDetailSeries" :value-formatter="formatRate" />
</div>
</template>
<div class="shrink-0 overflow-hidden rounded-lg border bg-card">
<div class="flex items-center gap-2 border-b px-3 py-2 text-xs font-medium">
<Activity class="h-3.5 w-3.5 text-muted-foreground" />
{{ t("nacos.dashboardClusterNodes") }}
<Badge variant="secondary" class="h-4 rounded px-1 text-[10px]">{{ snapshot?.nodes.length ?? 0 }}</Badge>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-xs">
<thead class="bg-muted/50 text-left text-muted-foreground">
<tr>
<th class="px-3 py-2 font-medium">{{ t("nacos.address") }}</th>
<th class="px-3 py-2 font-medium">{{ t("nacos.state") }}</th>
<th class="px-3 py-2 font-medium">{{ t("nacos.dashboardSite") }}</th>
<th class="px-3 py-2 font-medium">{{ t("nacos.weight") }}</th>
<th class="px-3 py-2 font-medium">{{ t("nacos.dashboardLastRefresh") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="node in snapshot?.nodes ?? []" :key="node.address" class="border-t hover:bg-accent/40">
<td class="px-3 py-2 font-mono">{{ node.address }}</td>
<td class="px-3 py-2">
<Badge :variant="isHealthyNacosNode(node) ? 'secondary' : 'destructive'" class="h-5 rounded-md px-1.5 text-[10px]">{{ nodeState(node) }}</Badge>
</td>
<td class="px-3 py-2 text-muted-foreground">{{ node.site || "—" }}</td>
<td class="px-3 py-2 tabular-nums text-muted-foreground">{{ node.weight ?? "—" }}</td>
<td class="px-3 py-2 text-muted-foreground">{{ nodeLastRefresh(node.lastRefreshTime) }}</td>
</tr>
<tr v-if="!snapshot?.nodes.length">
<td colspan="5" class="px-3 py-8 text-center text-muted-foreground">{{ t("nacos.dashboardNoNodes") }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<details v-if="snapshot?.warnings.length" class="shrink-0 rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs">
<summary class="cursor-pointer font-medium text-amber-700 dark:text-amber-300">{{ t("nacos.dashboardPartial", { count: snapshot.warnings.length }) }}</summary>
<ul class="mt-2 list-disc space-y-1 pl-5 text-muted-foreground">
<li v-for="warning in snapshot.warnings" :key="warning">{{ warning }}</li>
</ul>
</details>
</div>
</div>
</div>
</template>
@@ -1057,7 +1057,9 @@ async function openServerDashboard() {
try {
await connectionStore.ensureConnected(node.connectionId);
connectionStore.activeConnectionId = node.connectionId;
if (currentDatabaseType() === "postgres") {
if (currentDatabaseType() === "nacos") {
queryStore.openNacosDashboard(node.connectionId);
} else if (currentDatabaseType() === "postgres") {
queryStore.openPostgresDashboard(node.connectionId);
} else {
queryStore.openMysqlDashboard(node.connectionId);
@@ -3554,7 +3556,7 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
if (node.connectionId && connectionSupportsProcessList(connectionStore.getConfig(node.connectionId))) {
items.push({ label: t("contextMenu.processList"), action: openProcessList, icon: Activity });
}
if (node.connectionId && (connectionSupportsServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId)))) {
if (node.connectionId && (currentDatabaseType() === "nacos" || connectionSupportsServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId)))) {
items.push({ label: t("contextMenu.serverDashboard"), action: openServerDashboard, icon: Gauge });
}
if (currentDatabaseType() === "dameng") {
+80 -2
View File
@@ -341,9 +341,9 @@ export default {
nacosVersionAuto: "Auto detect",
nacosPrimaryAddressRNacos: "Nacos-compatible API address",
nacosPrimaryAddressV2: "Service address (API and console shared)",
nacosPrimaryAddressV3: "Admin API / console address",
nacosPrimaryAddressV3: "Server / Admin API address",
nacosPrimaryAddressAuto: "Nacos management address",
nacosPrimaryAddressHint: "Paste a full browser or API URL. Known Nacos 3 console routes are normalized; custom proxy prefixes are preserved.",
nacosPrimaryAddressHint: "For Nacos 3, use the Server / Admin API address (normally http://host:8848/nacos), not the port 8080 console; custom proxy prefixes are preserved.",
nacosRequestsUse: "Requests will use {address}/…",
nacosNamespace: "Namespace",
nacosContextPath: "Context Path",
@@ -373,6 +373,13 @@ export default {
nacosTls: "TLS",
nacosTlsSkipVerify: "Skip certificate verification",
nacosPageSize: "Page Size",
nacosMetrics: "Prometheus metrics",
nacosMetricsAuto: "Auto detect",
nacosMetricsDisabled: "Disabled",
nacosMetricsCustom: "Custom URL",
nacosMetricsAutoHint: "Tried in order: {addresses}",
nacosMetricsUrl: "Metrics URL",
nacosMetricsUrlInvalid: "Enter an absolute HTTP or HTTPS Prometheus metrics URL without credentials or a fragment.",
kafkaKerberosPrincipal: "Principal",
kafkaKerberosKeytab: "Keytab",
kafkaKerberosServiceName: "Service Name",
@@ -5496,6 +5503,77 @@ export default {
presetSchemaDesc: "Current topic schema definition",
},
nacos: {
dashboard: "Dashboard",
dashboardTitle: "Operations Dashboard",
dashboardUpdatedAt: "Updated {time}",
dashboardAutoRefresh: "Auto refresh",
dashboardOff: "Off",
dashboardNamespaces: "Namespaces",
dashboardConfigs: "Configs",
dashboardServices: "Services",
dashboardInstances: "Instances",
dashboardClients: "Clients",
dashboardConnections: "{count} connections",
dashboardNodes: "Healthy nodes",
dashboardCpu: "CPU",
dashboardMemory: "Memory",
dashboardLoad: "Load {value}",
dashboardCountsChart: "Service discovery",
dashboardResourcesChart: "Resource utilization",
dashboardClusterNodes: "Cluster nodes",
dashboardSite: "Site",
dashboardLastRefresh: "Last refresh",
dashboardNoNodes: "Cluster node details are unavailable",
dashboardPartial: "{count} dashboard sources are unavailable",
dashboardMemoryUsed: "Heap / memory used",
dashboardMemoryMax: "Maximum {value}",
dashboardRss: "RSS memory",
dashboardVms: "Virtual memory",
dashboardJvmThreads: "JVM daemon threads",
dashboardGcPauses: "GC pauses",
dashboardTrafficSection: "Request traffic",
dashboardRequestRateChart: "HTTP / gRPC requests per second",
dashboardLatencyChart: "Request latency",
dashboardHttpQps: "HTTP QPS",
dashboardGrpcQps: "gRPC QPS",
dashboardHttpAverage: "HTTP average",
dashboardHttpP50: "HTTP P50",
dashboardHttpP95: "HTTP P95",
dashboardHttpP99: "HTTP P99",
dashboardGrpcAverage: "gRPC average",
dashboardGrpcP50: "gRPC P50",
dashboardGrpcP95: "gRPC P95",
dashboardGrpcP99: "gRPC P99",
dashboardReliabilitySection: "Reliability",
dashboardErrorRateChart: "Request error rate",
dashboardExecutorChart: "gRPC executor",
dashboardHttpErrors: "HTTP errors",
dashboardGrpcErrors: "gRPC errors",
dashboardExecutorPool: "Pool size",
dashboardExecutorActive: "Active",
dashboardExecutorQueue: "Queued",
dashboardConfigSection: "Configuration center",
dashboardConfigRateChart: "Configuration operations per second",
dashboardConfigStateChart: "Listeners and pending work",
dashboardConfigGets: "Gets",
dashboardConfigPublishes: "Publishes",
dashboardLongPolling: "Long polling",
dashboardListenerClients: "Listener clients",
dashboardListenerKeys: "Listener keys",
dashboardNotifyTasks: "Notify tasks",
dashboardNotifyClientTasks: "Client notify tasks",
dashboardDumpTasks: "Dump tasks",
dashboardNamingSection: "Service discovery",
dashboardNamingStateChart: "Services, instances and clients",
dashboardPushRateChart: "Pushes per second",
dashboardPushDetailChart: "Push latency and pending work",
dashboardPushAverage: "Average latency",
dashboardPushMaximum: "Maximum latency",
dashboardPushPending: "Pending tasks",
dashboardSubscribers: "Subscribers",
dashboardPushes: "Pushes",
dashboardFailedPushes: "Failed pushes",
dashboardEmptyPushes: "Empty pushes",
configs: "Configs",
services: "Services",
raw: "Raw",
+80 -2
View File
@@ -328,9 +328,9 @@ export default withEnglishFallback({
nacosVersionAuto: "Detectar automáticamente",
nacosPrimaryAddressRNacos: "Dirección de API compatible con Nacos",
nacosPrimaryAddressV2: "Dirección del servicio (API y consola compartidas)",
nacosPrimaryAddressV3: "Dirección de API de administración / consola",
nacosPrimaryAddressV3: "Dirección de Server / Admin API",
nacosPrimaryAddressAuto: "Dirección de administración de Nacos",
nacosPrimaryAddressHint: "Pegue una URL completa del navegador o de la API. Las rutas conocidas de la consola de Nacos 3 se normalizan; se conservan los prefijos de proxy personalizados.",
nacosPrimaryAddressHint: "Para Nacos 3, use la dirección de Server / Admin API (normalmente http://host:8848/nacos), no la consola del puerto 8080; se conservan los prefijos proxy personalizados.",
nacosRequestsUse: "Las solicitudes usarán {address}/…",
nacosNamespace: "Namespace",
nacosContextPath: "Ruta de contexto",
@@ -360,6 +360,13 @@ export default withEnglishFallback({
nacosTls: "TLS",
nacosTlsSkipVerify: "Omitir verificación de certificado",
nacosPageSize: "Tamaño de página",
nacosMetrics: "Métricas Prometheus",
nacosMetricsAuto: "Detección automática",
nacosMetricsDisabled: "Desactivado",
nacosMetricsCustom: "URL personalizada",
nacosMetricsAutoHint: "Se probarán en orden: {addresses}",
nacosMetricsUrl: "URL de métricas",
nacosMetricsUrlInvalid: "Introduce una URL HTTP o HTTPS absoluta sin credenciales ni fragmento.",
searchDatabasePlaceholder: "Buscar tipos de bases de datos",
searchResults: "Resultados de búsqueda",
databaseCategories: "Categorías",
@@ -4278,6 +4285,77 @@ export default withEnglishFallback({
runInBackground: "Ejecutar en segundo plano",
},
nacos: {
dashboard: "Panel",
dashboardTitle: "Panel de operaciones",
dashboardUpdatedAt: "Actualizado {time}",
dashboardAutoRefresh: "Actualización automática",
dashboardOff: "Desactivada",
dashboardNamespaces: "Espacios de nombres",
dashboardConfigs: "Configuraciones",
dashboardServices: "Servicios",
dashboardInstances: "Instancias",
dashboardClients: "Clientes",
dashboardConnections: "{count} conexiones",
dashboardNodes: "Nodos saludables",
dashboardCpu: "CPU",
dashboardMemory: "Memoria",
dashboardLoad: "Carga {value}",
dashboardCountsChart: "Descubrimiento de servicios",
dashboardResourcesChart: "Uso de recursos",
dashboardClusterNodes: "Nodos del clúster",
dashboardSite: "Sitio",
dashboardLastRefresh: "Última actualización",
dashboardNoNodes: "Los detalles de los nodos no están disponibles",
dashboardPartial: "{count} fuentes de datos no están disponibles",
dashboardMemoryUsed: "Heap / memoria usada",
dashboardMemoryMax: "Máximo {value}",
dashboardRss: "Memoria RSS",
dashboardVms: "Memoria virtual",
dashboardJvmThreads: "Hilos daemon JVM",
dashboardGcPauses: "Pausas GC",
dashboardTrafficSection: "Tráfico de solicitudes",
dashboardRequestRateChart: "Solicitudes HTTP / gRPC por segundo",
dashboardLatencyChart: "Latencia de solicitudes",
dashboardHttpQps: "QPS HTTP",
dashboardGrpcQps: "QPS gRPC",
dashboardHttpAverage: "Media HTTP",
dashboardHttpP50: "HTTP P50",
dashboardHttpP95: "HTTP P95",
dashboardHttpP99: "HTTP P99",
dashboardGrpcAverage: "Media gRPC",
dashboardGrpcP50: "gRPC P50",
dashboardGrpcP95: "gRPC P95",
dashboardGrpcP99: "gRPC P99",
dashboardReliabilitySection: "Fiabilidad",
dashboardErrorRateChart: "Tasa de errores",
dashboardExecutorChart: "Ejecutor gRPC",
dashboardHttpErrors: "Errores HTTP",
dashboardGrpcErrors: "Errores gRPC",
dashboardExecutorPool: "Tamaño del pool",
dashboardExecutorActive: "Activos",
dashboardExecutorQueue: "En cola",
dashboardConfigSection: "Centro de configuración",
dashboardConfigRateChart: "Operaciones de configuración por segundo",
dashboardConfigStateChart: "Listeners y trabajo pendiente",
dashboardConfigGets: "Consultas",
dashboardConfigPublishes: "Publicaciones",
dashboardLongPolling: "Sondeo largo",
dashboardListenerClients: "Clientes listener",
dashboardListenerKeys: "Claves listener",
dashboardNotifyTasks: "Tareas de notificación",
dashboardNotifyClientTasks: "Notificaciones a clientes",
dashboardDumpTasks: "Tareas dump",
dashboardNamingSection: "Descubrimiento de servicios",
dashboardNamingStateChart: "Servicios, instancias y clientes",
dashboardPushRateChart: "Envíos por segundo",
dashboardPushDetailChart: "Latencia y tareas pendientes de envío",
dashboardPushAverage: "Latencia media",
dashboardPushMaximum: "Latencia máxima",
dashboardPushPending: "Tareas pendientes",
dashboardSubscribers: "Suscriptores",
dashboardPushes: "Envíos",
dashboardFailedPushes: "Envíos fallidos",
dashboardEmptyPushes: "Envíos vacíos",
configs: "Configuraciones",
services: "Servicios",
raw: "Raw",
+80 -2
View File
@@ -327,9 +327,9 @@ export default withEnglishFallback({
nacosVersionAuto: "Rileva automaticamente",
nacosPrimaryAddressRNacos: "Indirizzo API compatibile con Nacos",
nacosPrimaryAddressV2: "Indirizzo del servizio (API e console condivise)",
nacosPrimaryAddressV3: "Indirizzo API di amministrazione / console",
nacosPrimaryAddressV3: "Indirizzo Server / Admin API",
nacosPrimaryAddressAuto: "Indirizzo di amministrazione Nacos",
nacosPrimaryAddressHint: "Incolla un URL completo del browser o dell'API. I percorsi noti della console Nacos 3 vengono normalizzati; i prefissi proxy personalizzati vengono mantenuti.",
nacosPrimaryAddressHint: "Per Nacos 3 usa l'indirizzo Server / Admin API (normalmente http://host:8848/nacos), non la console sulla porta 8080; i prefissi proxy personalizzati vengono mantenuti.",
nacosRequestsUse: "Le richieste useranno {address}/…",
nacosNamespace: "Namespace",
nacosContextPath: "Percorso di contesto",
@@ -359,6 +359,13 @@ export default withEnglishFallback({
nacosTls: "TLS",
nacosTlsSkipVerify: "Salta verifica certificato",
nacosPageSize: "Dimensione pagina",
nacosMetrics: "Metriche Prometheus",
nacosMetricsAuto: "Rilevamento automatico",
nacosMetricsDisabled: "Disabilitato",
nacosMetricsCustom: "URL personalizzato",
nacosMetricsAutoHint: "Verranno provati in ordine: {addresses}",
nacosMetricsUrl: "URL metriche",
nacosMetricsUrlInvalid: "Inserisci un URL HTTP o HTTPS assoluto senza credenziali o frammento.",
searchDatabasePlaceholder: "Cerca tipi di database",
searchResults: "Risultati della ricerca",
databaseCategories: "Categorie",
@@ -4276,6 +4283,77 @@ export default withEnglishFallback({
runInBackground: "Esegui in background",
},
nacos: {
dashboard: "Dashboard",
dashboardTitle: "Dashboard operativa",
dashboardUpdatedAt: "Aggiornato {time}",
dashboardAutoRefresh: "Aggiornamento automatico",
dashboardOff: "Disattivato",
dashboardNamespaces: "Namespace",
dashboardConfigs: "Configurazioni",
dashboardServices: "Servizi",
dashboardInstances: "Istanze",
dashboardClients: "Client",
dashboardConnections: "{count} connessioni",
dashboardNodes: "Nodi integri",
dashboardCpu: "CPU",
dashboardMemory: "Memoria",
dashboardLoad: "Carico {value}",
dashboardCountsChart: "Rilevamento servizi",
dashboardResourcesChart: "Utilizzo risorse",
dashboardClusterNodes: "Nodi del cluster",
dashboardSite: "Sito",
dashboardLastRefresh: "Ultimo aggiornamento",
dashboardNoNodes: "I dettagli dei nodi non sono disponibili",
dashboardPartial: "{count} origini dati non sono disponibili",
dashboardMemoryUsed: "Heap / memoria usata",
dashboardMemoryMax: "Massimo {value}",
dashboardRss: "Memoria RSS",
dashboardVms: "Memoria virtuale",
dashboardJvmThreads: "Thread daemon JVM",
dashboardGcPauses: "Pause GC",
dashboardTrafficSection: "Traffico richieste",
dashboardRequestRateChart: "Richieste HTTP / gRPC al secondo",
dashboardLatencyChart: "Latenza richieste",
dashboardHttpQps: "QPS HTTP",
dashboardGrpcQps: "QPS gRPC",
dashboardHttpAverage: "Media HTTP",
dashboardHttpP50: "HTTP P50",
dashboardHttpP95: "HTTP P95",
dashboardHttpP99: "HTTP P99",
dashboardGrpcAverage: "Media gRPC",
dashboardGrpcP50: "gRPC P50",
dashboardGrpcP95: "gRPC P95",
dashboardGrpcP99: "gRPC P99",
dashboardReliabilitySection: "Affidabilità",
dashboardErrorRateChart: "Tasso di errore",
dashboardExecutorChart: "Executor gRPC",
dashboardHttpErrors: "Errori HTTP",
dashboardGrpcErrors: "Errori gRPC",
dashboardExecutorPool: "Dimensione pool",
dashboardExecutorActive: "Attivi",
dashboardExecutorQueue: "In coda",
dashboardConfigSection: "Centro configurazioni",
dashboardConfigRateChart: "Operazioni di configurazione al secondo",
dashboardConfigStateChart: "Listener e lavoro in attesa",
dashboardConfigGets: "Letture",
dashboardConfigPublishes: "Pubblicazioni",
dashboardLongPolling: "Long polling",
dashboardListenerClients: "Client listener",
dashboardListenerKeys: "Chiavi listener",
dashboardNotifyTasks: "Attività di notifica",
dashboardNotifyClientTasks: "Notifiche client",
dashboardDumpTasks: "Attività dump",
dashboardNamingSection: "Service discovery",
dashboardNamingStateChart: "Servizi, istanze e client",
dashboardPushRateChart: "Push al secondo",
dashboardPushDetailChart: "Latenza push e attività in attesa",
dashboardPushAverage: "Latenza media",
dashboardPushMaximum: "Latenza massima",
dashboardPushPending: "Attività in attesa",
dashboardSubscribers: "Sottoscrittori",
dashboardPushes: "Push",
dashboardFailedPushes: "Push falliti",
dashboardEmptyPushes: "Push vuoti",
configs: "Configurazioni",
services: "Servizi",
raw: "Raw",
+80 -2
View File
@@ -321,9 +321,9 @@ export default withEnglishFallback({
nacosVersionAuto: "自動検出",
nacosPrimaryAddressRNacos: "Nacos 互換 API アドレス",
nacosPrimaryAddressV2: "サービスアドレス(API とコンソールで共有)",
nacosPrimaryAddressV3: "管理 API / コンソールアドレス",
nacosPrimaryAddressV3: "Server / Admin API アドレス",
nacosPrimaryAddressAuto: "Nacos 管理アドレス",
nacosPrimaryAddressHint: "完全なブラウザーまたは API URL を貼り付けてください。既知の Nacos 3 コンソールルートは正規化され、カスタムプロキシプレフィックスは保持されます。",
nacosPrimaryAddressHint: "Nacos 3 では、8080 のコンソールではなく Server / Admin API アドレス(通常 http://host:8848/nacos)を指定してください。カスタムプロキシプレフィックスは保持されます。",
nacosRequestsUse: "リクエストには {address}/… を使用します",
nacosNamespace: "名前空間",
nacosContextPath: "コンテキストパス",
@@ -353,6 +353,13 @@ export default withEnglishFallback({
nacosTls: "TLS",
nacosTlsSkipVerify: "証明書検証をスキップ",
nacosPageSize: "ページサイズ",
nacosMetrics: "Prometheus メトリクス",
nacosMetricsAuto: "自動検出",
nacosMetricsDisabled: "無効",
nacosMetricsCustom: "カスタム URL",
nacosMetricsAutoHint: "次の順序で試行します:{addresses}",
nacosMetricsUrl: "メトリクス URL",
nacosMetricsUrlInvalid: "認証情報やフラグメントを含まない HTTP または HTTPS の絶対 URL を入力してください。",
searchDatabasePlaceholder: "データベースタイプを検索",
searchResults: "検索結果",
databaseCategories: "カテゴリ",
@@ -4276,6 +4283,77 @@ export default withEnglishFallback({
exportCancelled: "エクスポートがキャンセルされました",
},
nacos: {
dashboard: "ダッシュボード",
dashboardTitle: "運用ダッシュボード",
dashboardUpdatedAt: "更新 {time}",
dashboardAutoRefresh: "自動更新",
dashboardOff: "オフ",
dashboardNamespaces: "名前空間",
dashboardConfigs: "設定数",
dashboardServices: "サービス数",
dashboardInstances: "インスタンス数",
dashboardClients: "クライアント",
dashboardConnections: "{count} 接続",
dashboardNodes: "正常ノード",
dashboardCpu: "CPU",
dashboardMemory: "メモリ",
dashboardLoad: "負荷 {value}",
dashboardCountsChart: "サービスディスカバリ",
dashboardResourcesChart: "リソース使用率",
dashboardClusterNodes: "クラスターノード",
dashboardSite: "サイト",
dashboardLastRefresh: "最終更新",
dashboardNoNodes: "クラスターノードの詳細を取得できません",
dashboardPartial: "{count} 件のデータソースを利用できません",
dashboardMemoryUsed: "ヒープ / メモリ使用量",
dashboardMemoryMax: "上限 {value}",
dashboardRss: "RSS メモリ",
dashboardVms: "仮想メモリ",
dashboardJvmThreads: "JVM デーモンスレッド",
dashboardGcPauses: "GC 一時停止回数",
dashboardTrafficSection: "リクエストトラフィック",
dashboardRequestRateChart: "HTTP / gRPC リクエスト/秒",
dashboardLatencyChart: "リクエスト遅延",
dashboardHttpQps: "HTTP QPS",
dashboardGrpcQps: "gRPC QPS",
dashboardHttpAverage: "HTTP 平均",
dashboardHttpP50: "HTTP P50",
dashboardHttpP95: "HTTP P95",
dashboardHttpP99: "HTTP P99",
dashboardGrpcAverage: "gRPC 平均",
dashboardGrpcP50: "gRPC P50",
dashboardGrpcP95: "gRPC P95",
dashboardGrpcP99: "gRPC P99",
dashboardReliabilitySection: "信頼性",
dashboardErrorRateChart: "リクエストエラー率",
dashboardExecutorChart: "gRPC エグゼキューター",
dashboardHttpErrors: "HTTP エラー",
dashboardGrpcErrors: "gRPC エラー",
dashboardExecutorPool: "プールサイズ",
dashboardExecutorActive: "アクティブ",
dashboardExecutorQueue: "待機中",
dashboardConfigSection: "設定センター",
dashboardConfigRateChart: "設定操作/秒",
dashboardConfigStateChart: "リスナーと保留タスク",
dashboardConfigGets: "取得",
dashboardConfigPublishes: "公開",
dashboardLongPolling: "ロングポーリング",
dashboardListenerClients: "リスナークライアント",
dashboardListenerKeys: "リスナーキー",
dashboardNotifyTasks: "通知タスク",
dashboardNotifyClientTasks: "クライアント通知タスク",
dashboardDumpTasks: "ダンプタスク",
dashboardNamingSection: "サービスディスカバリ",
dashboardNamingStateChart: "サービス、インスタンス、クライアント",
dashboardPushRateChart: "プッシュ/秒",
dashboardPushDetailChart: "プッシュ遅延と保留タスク",
dashboardPushAverage: "平均遅延",
dashboardPushMaximum: "最大遅延",
dashboardPushPending: "保留タスク",
dashboardSubscribers: "サブスクライバー",
dashboardPushes: "プッシュ",
dashboardFailedPushes: "失敗プッシュ",
dashboardEmptyPushes: "空プッシュ",
configs: "設定",
services: "サービス",
raw: "Raw",
+80 -2
View File
@@ -328,9 +328,9 @@ export default withEnglishFallback({
nacosVersionAuto: "Detectar automaticamente",
nacosPrimaryAddressRNacos: "Endereço de API compatível com Nacos",
nacosPrimaryAddressV2: "Endereço do serviço (API e console compartilhados)",
nacosPrimaryAddressV3: "Endereço de API administrativa / console",
nacosPrimaryAddressV3: "Endereço de Server / Admin API",
nacosPrimaryAddressAuto: "Endereço de administração do Nacos",
nacosPrimaryAddressHint: "Cole uma URL completa do navegador ou da API. As rotas conhecidas do console do Nacos 3 são normalizadas; prefixos de proxy personalizados são preservados.",
nacosPrimaryAddressHint: "No Nacos 3, use o endereço de Server / Admin API (normalmente http://host:8848/nacos), não o console na porta 8080; prefixos de proxy personalizados são preservados.",
nacosRequestsUse: "As solicitações usarão {address}/…",
nacosNamespace: "Namespace",
nacosContextPath: "Caminho de contexto",
@@ -360,6 +360,13 @@ export default withEnglishFallback({
nacosTls: "TLS",
nacosTlsSkipVerify: "Ignorar verificação do certificado",
nacosPageSize: "Tamanho da página",
nacosMetrics: "Métricas Prometheus",
nacosMetricsAuto: "Detecção automática",
nacosMetricsDisabled: "Desativado",
nacosMetricsCustom: "URL personalizada",
nacosMetricsAutoHint: "Tentativas em ordem: {addresses}",
nacosMetricsUrl: "URL de métricas",
nacosMetricsUrlInvalid: "Informe uma URL HTTP ou HTTPS absoluta, sem credenciais ou fragmento.",
searchDatabasePlaceholder: "Pesquisar tipos de banco de dados",
searchResults: "Resultados da busca",
databaseCategories: "Categorias",
@@ -4278,6 +4285,77 @@ export default withEnglishFallback({
exportCancelled: "Exportação cancelada",
},
nacos: {
dashboard: "Painel",
dashboardTitle: "Painel de operações",
dashboardUpdatedAt: "Atualizado {time}",
dashboardAutoRefresh: "Atualização automática",
dashboardOff: "Desativada",
dashboardNamespaces: "Namespaces",
dashboardConfigs: "Configurações",
dashboardServices: "Serviços",
dashboardInstances: "Instâncias",
dashboardClients: "Clientes",
dashboardConnections: "{count} conexões",
dashboardNodes: "Nós saudáveis",
dashboardCpu: "CPU",
dashboardMemory: "Memória",
dashboardLoad: "Carga {value}",
dashboardCountsChart: "Descoberta de serviços",
dashboardResourcesChart: "Uso de recursos",
dashboardClusterNodes: "Nós do cluster",
dashboardSite: "Site",
dashboardLastRefresh: "Última atualização",
dashboardNoNodes: "Os detalhes dos nós não estão disponíveis",
dashboardPartial: "{count} fontes de dados não estão disponíveis",
dashboardMemoryUsed: "Heap / memória usada",
dashboardMemoryMax: "Máximo {value}",
dashboardRss: "Memória RSS",
dashboardVms: "Memória virtual",
dashboardJvmThreads: "Threads daemon da JVM",
dashboardGcPauses: "Pausas de GC",
dashboardTrafficSection: "Tráfego de requisições",
dashboardRequestRateChart: "Requisições HTTP / gRPC por segundo",
dashboardLatencyChart: "Latência das requisições",
dashboardHttpQps: "QPS HTTP",
dashboardGrpcQps: "QPS gRPC",
dashboardHttpAverage: "Média HTTP",
dashboardHttpP50: "HTTP P50",
dashboardHttpP95: "HTTP P95",
dashboardHttpP99: "HTTP P99",
dashboardGrpcAverage: "Média gRPC",
dashboardGrpcP50: "gRPC P50",
dashboardGrpcP95: "gRPC P95",
dashboardGrpcP99: "gRPC P99",
dashboardReliabilitySection: "Confiabilidade",
dashboardErrorRateChart: "Taxa de erros",
dashboardExecutorChart: "Executor gRPC",
dashboardHttpErrors: "Erros HTTP",
dashboardGrpcErrors: "Erros gRPC",
dashboardExecutorPool: "Tamanho do pool",
dashboardExecutorActive: "Ativos",
dashboardExecutorQueue: "Na fila",
dashboardConfigSection: "Central de configuração",
dashboardConfigRateChart: "Operações de configuração por segundo",
dashboardConfigStateChart: "Listeners e trabalho pendente",
dashboardConfigGets: "Consultas",
dashboardConfigPublishes: "Publicações",
dashboardLongPolling: "Long polling",
dashboardListenerClients: "Clientes listener",
dashboardListenerKeys: "Chaves listener",
dashboardNotifyTasks: "Tarefas de notificação",
dashboardNotifyClientTasks: "Notificações de clientes",
dashboardDumpTasks: "Tarefas dump",
dashboardNamingSection: "Descoberta de serviços",
dashboardNamingStateChart: "Serviços, instâncias e clientes",
dashboardPushRateChart: "Pushes por segundo",
dashboardPushDetailChart: "Latência e tarefas pendentes de push",
dashboardPushAverage: "Latência média",
dashboardPushMaximum: "Latência máxima",
dashboardPushPending: "Tarefas pendentes",
dashboardSubscribers: "Assinantes",
dashboardPushes: "Pushes",
dashboardFailedPushes: "Pushes com falha",
dashboardEmptyPushes: "Pushes vazios",
configs: "Configurações",
services: "Serviços",
raw: "Raw",
+80 -2
View File
@@ -343,9 +343,9 @@ export default withEnglishFallback({
nacosVersionAuto: "自动检测",
nacosPrimaryAddressRNacos: "兼容 Nacos 的 API 地址",
nacosPrimaryAddressV2: "服务地址(API 与控制台共用)",
nacosPrimaryAddressV3: "管理 API / 控制台地址",
nacosPrimaryAddressV3: "Server / Admin API 地址",
nacosPrimaryAddressAuto: "Nacos 管理地址",
nacosPrimaryAddressHint: "可粘贴完整的浏览器或 API URL。已知的 Nacos 3 控制台路径会自动规范化;自定义代理前缀会被保留。",
nacosPrimaryAddressHint: "Nacos 3 请填写 Server / Admin API 地址(默认 http://主机:8848/nacos),不要填写 8080 控制台地址;自定义代理前缀会被保留。",
nacosRequestsUse: "请求将使用 {address}/…",
nacosNamespace: "命名空间",
nacosContextPath: "上下文路径",
@@ -374,6 +374,13 @@ export default withEnglishFallback({
nacosTls: "TLS",
nacosTlsSkipVerify: "跳过证书验证",
nacosPageSize: "分页大小",
nacosMetrics: "Prometheus 指标",
nacosMetricsAuto: "自动探测",
nacosMetricsDisabled: "关闭",
nacosMetricsCustom: "自定义地址",
nacosMetricsAutoHint: "将按顺序尝试:{addresses}",
nacosMetricsUrl: "指标地址",
nacosMetricsUrlInvalid: "请输入不含凭据和片段的 HTTP 或 HTTPS Prometheus 指标绝对地址。",
kafkaKerberosPrincipal: "Principal",
kafkaKerberosKeytab: "Keytab",
kafkaKerberosServiceName: "服务名",
@@ -5494,6 +5501,77 @@ export default withEnglishFallback({
presetSchemaDesc: "查看主题当前 schema 定义",
},
nacos: {
dashboard: "大盘",
dashboardTitle: "运行大盘",
dashboardUpdatedAt: "更新于 {time}",
dashboardAutoRefresh: "自动刷新",
dashboardOff: "关闭",
dashboardNamespaces: "命名空间",
dashboardConfigs: "配置数",
dashboardServices: "服务数",
dashboardInstances: "实例数",
dashboardClients: "客户端",
dashboardConnections: "{count} 个连接",
dashboardNodes: "健康节点",
dashboardCpu: "CPU",
dashboardMemory: "内存",
dashboardLoad: "负载 {value}",
dashboardCountsChart: "服务发现指标",
dashboardResourcesChart: "资源使用率",
dashboardClusterNodes: "集群节点",
dashboardSite: "站点",
dashboardLastRefresh: "最后刷新",
dashboardNoNodes: "当前无法获取集群节点明细",
dashboardPartial: "有 {count} 项大盘数据源不可用",
dashboardMemoryUsed: "堆 / 内存已用",
dashboardMemoryMax: "上限 {value}",
dashboardRss: "RSS 内存",
dashboardVms: "虚拟内存",
dashboardJvmThreads: "JVM 守护线程",
dashboardGcPauses: "GC 暂停次数",
dashboardTrafficSection: "请求流量",
dashboardRequestRateChart: "HTTP / gRPC 每秒请求",
dashboardLatencyChart: "请求延迟",
dashboardHttpQps: "HTTP QPS",
dashboardGrpcQps: "gRPC QPS",
dashboardHttpAverage: "HTTP 平均",
dashboardHttpP50: "HTTP P50",
dashboardHttpP95: "HTTP P95",
dashboardHttpP99: "HTTP P99",
dashboardGrpcAverage: "gRPC 平均",
dashboardGrpcP50: "gRPC P50",
dashboardGrpcP95: "gRPC P95",
dashboardGrpcP99: "gRPC P99",
dashboardReliabilitySection: "稳定性",
dashboardErrorRateChart: "请求错误率",
dashboardExecutorChart: "gRPC 执行器",
dashboardHttpErrors: "HTTP 错误",
dashboardGrpcErrors: "gRPC 错误",
dashboardExecutorPool: "线程池",
dashboardExecutorActive: "活跃线程",
dashboardExecutorQueue: "排队任务",
dashboardConfigSection: "配置中心",
dashboardConfigRateChart: "配置操作速率",
dashboardConfigStateChart: "监听与积压",
dashboardConfigGets: "查询配置",
dashboardConfigPublishes: "发布配置",
dashboardLongPolling: "长轮询",
dashboardListenerClients: "监听客户端",
dashboardListenerKeys: "监听键",
dashboardNotifyTasks: "通知任务",
dashboardNotifyClientTasks: "客户端通知任务",
dashboardDumpTasks: "Dump 任务",
dashboardNamingSection: "服务发现",
dashboardNamingStateChart: "服务、实例与客户端",
dashboardPushRateChart: "每秒推送",
dashboardPushDetailChart: "推送延迟与积压",
dashboardPushAverage: "平均延迟",
dashboardPushMaximum: "最大延迟",
dashboardPushPending: "待处理任务",
dashboardSubscribers: "订阅者",
dashboardPushes: "推送",
dashboardFailedPushes: "失败推送",
dashboardEmptyPushes: "空推送",
configs: "配置",
services: "服务",
raw: "Raw",
+80 -2
View File
@@ -328,9 +328,9 @@ export default withEnglishFallback({
nacosVersionAuto: "自動偵測",
nacosPrimaryAddressRNacos: "相容 Nacos 的 API 位址",
nacosPrimaryAddressV2: "服務位址(API 與主控台共用)",
nacosPrimaryAddressV3: "管理 API / 主控台位址",
nacosPrimaryAddressV3: "Server / Admin API 位址",
nacosPrimaryAddressAuto: "Nacos 管理位址",
nacosPrimaryAddressHint: "可貼上完整的瀏覽器或 API URL。已知的 Nacos 3 主控台路徑會自動正規化;自訂代理前綴會被保留。",
nacosPrimaryAddressHint: "Nacos 3 請填寫 Server / Admin API 位址(預設 http://主機:8848/nacos),不要填寫 8080 主控台位址;自訂代理前綴會被保留。",
nacosRequestsUse: "請求將使用 {address}/…",
nacosNamespace: "命名空間",
nacosContextPath: "上下文路徑",
@@ -359,6 +359,13 @@ export default withEnglishFallback({
nacosTls: "TLS",
nacosTlsSkipVerify: "跳過憑證驗證",
nacosPageSize: "分頁大小",
nacosMetrics: "Prometheus 指標",
nacosMetricsAuto: "自動偵測",
nacosMetricsDisabled: "關閉",
nacosMetricsCustom: "自訂位址",
nacosMetricsAutoHint: "將依序嘗試:{addresses}",
nacosMetricsUrl: "指標位址",
nacosMetricsUrlInvalid: "請輸入不含認證資訊與片段的 HTTP 或 HTTPS Prometheus 指標絕對位址。",
searchDatabasePlaceholder: "搜尋資料庫類型",
searchResults: "搜尋結果",
databaseCategories: "分類",
@@ -4087,6 +4094,77 @@ export default withEnglishFallback({
runInBackground: "背景執行",
},
nacos: {
dashboard: "儀表板",
dashboardTitle: "運行儀表板",
dashboardUpdatedAt: "更新於 {time}",
dashboardAutoRefresh: "自動重新整理",
dashboardOff: "關閉",
dashboardNamespaces: "命名空間",
dashboardConfigs: "設定數",
dashboardServices: "服務數",
dashboardInstances: "實例數",
dashboardClients: "用戶端",
dashboardConnections: "{count} 個連線",
dashboardNodes: "健康節點",
dashboardCpu: "CPU",
dashboardMemory: "記憶體",
dashboardLoad: "負載 {value}",
dashboardCountsChart: "服務發現指標",
dashboardResourcesChart: "資源使用率",
dashboardClusterNodes: "叢集節點",
dashboardSite: "站點",
dashboardLastRefresh: "最後重新整理",
dashboardNoNodes: "目前無法取得叢集節點明細",
dashboardPartial: "有 {count} 項儀表板資料來源無法使用",
dashboardMemoryUsed: "堆積 / 記憶體已用",
dashboardMemoryMax: "上限 {value}",
dashboardRss: "RSS 記憶體",
dashboardVms: "虛擬記憶體",
dashboardJvmThreads: "JVM 守護執行緒",
dashboardGcPauses: "GC 暫停次數",
dashboardTrafficSection: "請求流量",
dashboardRequestRateChart: "HTTP / gRPC 每秒請求",
dashboardLatencyChart: "請求延遲",
dashboardHttpQps: "HTTP QPS",
dashboardGrpcQps: "gRPC QPS",
dashboardHttpAverage: "HTTP 平均",
dashboardHttpP50: "HTTP P50",
dashboardHttpP95: "HTTP P95",
dashboardHttpP99: "HTTP P99",
dashboardGrpcAverage: "gRPC 平均",
dashboardGrpcP50: "gRPC P50",
dashboardGrpcP95: "gRPC P95",
dashboardGrpcP99: "gRPC P99",
dashboardReliabilitySection: "穩定性",
dashboardErrorRateChart: "請求錯誤率",
dashboardExecutorChart: "gRPC 執行器",
dashboardHttpErrors: "HTTP 錯誤",
dashboardGrpcErrors: "gRPC 錯誤",
dashboardExecutorPool: "執行緒池",
dashboardExecutorActive: "活躍執行緒",
dashboardExecutorQueue: "排隊任務",
dashboardConfigSection: "設定中心",
dashboardConfigRateChart: "設定操作速率",
dashboardConfigStateChart: "監聽與積壓",
dashboardConfigGets: "查詢設定",
dashboardConfigPublishes: "發布設定",
dashboardLongPolling: "長輪詢",
dashboardListenerClients: "監聽用戶端",
dashboardListenerKeys: "監聽鍵",
dashboardNotifyTasks: "通知任務",
dashboardNotifyClientTasks: "用戶端通知任務",
dashboardDumpTasks: "Dump 任務",
dashboardNamingSection: "服務發現",
dashboardNamingStateChart: "服務、實例與用戶端",
dashboardPushRateChart: "每秒推送",
dashboardPushDetailChart: "推送延遲與積壓",
dashboardPushAverage: "平均延遲",
dashboardPushMaximum: "最大延遲",
dashboardPushPending: "待處理任務",
dashboardSubscribers: "訂閱者",
dashboardPushes: "推送",
dashboardFailedPushes: "失敗推送",
dashboardEmptyPushes: "空推送",
configs: "配置",
services: "服務",
raw: "Raw",
+31
View File
@@ -18,6 +18,8 @@ import {
isNacosConfigSaveSnapshotCurrent,
isNacosConfigDeleteSnapshotInScope,
nacosConfigFileExtension,
nacosMetricsCandidates,
normalizeNacosMetricsUrl,
parseNacosRawBody,
parseNacosRawQuery,
normalizeNacosEndpoint,
@@ -41,12 +43,41 @@ describe("nacosAdmin helpers", () => {
contextPath: "/gateway",
detectedVersion: "v3",
});
expect(normalizeNacosEndpoint("http://127.0.0.1:8848", { implementation: "nacos", versionMode: "v3" })).toMatchObject({
serverAddr: "http://127.0.0.1:8848",
contextPath: "/nacos",
detectedVersion: "v3",
});
expect(normalizeNacosEndpoint("http://127.0.0.1:8848", { implementation: "nacos", versionMode: "v3", contextPath: "/" })).toMatchObject({
serverAddr: "http://127.0.0.1:8848",
contextPath: "/",
detectedVersion: "v3",
});
const savedAutoRootEndpoint = normalizeNacosEndpoint("http://127.0.0.1:8080", { implementation: "nacos", versionMode: "auto" });
expect(savedAutoRootEndpoint).toMatchObject({ serverAddr: "http://127.0.0.1:8080", contextPath: "" });
expect(
normalizeNacosEndpoint(savedAutoRootEndpoint.serverAddr, {
implementation: "nacos",
versionMode: "auto",
contextPath: savedAutoRootEndpoint.contextPath || undefined,
}),
).toMatchObject({ serverAddr: "http://127.0.0.1:8080", contextPath: "" });
expect(normalizeNacosEndpoint("http://rnacos.example:8848/nacos", { implementation: "rnacos" })).toMatchObject({
serverAddr: "http://rnacos.example:8848",
contextPath: "/nacos",
});
expect(() => normalizeNacosEndpoint("http://user:secret@nacos.example", { implementation: "nacos" })).toThrow(/embedded credentials/i);
});
it("derives and validates Prometheus endpoints", () => {
expect(nacosMetricsCandidates("http://127.0.0.1:8818", "/nacos", "nacos")).toEqual(["http://127.0.0.1:8818/nacos/actuator/prometheus", "http://127.0.0.1:8818/actuator/prometheus"]);
expect(nacosMetricsCandidates("http://127.0.0.1:3848", "/nacos", "rnacos")).toEqual(["http://127.0.0.1:3848/metrics", "http://127.0.0.1:3848/nacos/metrics", "http://127.0.0.1:3848/rnacos/metrics"]);
expect(normalizeNacosMetricsUrl("http://localhost:8818/metrics?node=a")).toBe("http://localhost:8818/metrics?node=a");
expect(() => normalizeNacosMetricsUrl("file:///tmp/metrics")).toThrow(/HTTP or HTTPS/);
expect(() => normalizeNacosMetricsUrl("http://user:secret@localhost/metrics")).toThrow(/credentials/);
expect(() => normalizeNacosMetricsUrl("http://localhost/metrics#fragment")).toThrow(/fragment/);
expect(() => normalizeNacosMetricsUrl("http://localhost/metrics#")).toThrow(/fragment/);
});
it("parses raw query and body text", () => {
expect(parseNacosRawQuery("?dataId=a&group=DEFAULT_GROUP")).toEqual({ dataId: "a", group: "DEFAULT_GROUP" });
expect(parseNacosRawQuery("")).toBeUndefined();
@@ -0,0 +1,144 @@
import { describe, expect, it } from "vitest";
import { appendDashboardSample, averageDurationMsSeries, counterRateSeries, dashboardMetric, dashboardNamespaceLabel, dashboardSeries, errorRateSeries, formatDashboardPercent, isHealthyNacosNode, ratioPercent, type NacosDashboardSample } from "@/lib/nacos/nacosDashboard";
function sample(at: number, metrics: NacosDashboardSample["snapshot"]["metrics"]): NacosDashboardSample {
return {
at,
snapshot: {
namespace: "",
metrics,
nodes: [],
warnings: [],
},
};
}
describe("nacosDashboard helpers", () => {
it("reads metric series and leaves gaps for missing values", () => {
const samples = [sample(1, { serviceCount: 2 }), sample(2, { serviceCount: 5 }), sample(3, undefined)];
expect(dashboardMetric(samples[1], "serviceCount")).toBe(5);
expect(dashboardSeries(samples, "serviceCount")).toEqual([2, 5, null]);
});
it("calculates rates and leaves gaps after counter resets", () => {
const samples = [
{ ...sample(1_000, {}), snapshot: { ...sample(1_000, {}).snapshot, prometheus: { source: { kind: "nacos" as const, endpoint: "http://metrics" }, resource: {}, traffic: { httpRequestsTotal: 10 }, config: {}, naming: {} } } },
{ ...sample(3_000, {}), snapshot: { ...sample(3_000, {}).snapshot, prometheus: { source: { kind: "nacos" as const, endpoint: "http://metrics" }, resource: {}, traffic: { httpRequestsTotal: 20 }, config: {}, naming: {} } } },
{ ...sample(5_000, {}), snapshot: { ...sample(5_000, {}).snapshot, prometheus: { source: { kind: "nacos" as const, endpoint: "http://metrics" }, resource: {}, traffic: { httpRequestsTotal: 2 }, config: {}, naming: {} } } },
];
expect(counterRateSeries(samples, (item) => item.snapshot.prometheus?.traffic.httpRequestsTotal)).toEqual([null, 5, null]);
});
it("leaves counter-derived gaps when the Prometheus source changes", () => {
const samples: NacosDashboardSample[] = [
{
...sample(1_000, {}),
snapshot: {
...sample(1_000, {}).snapshot,
prometheus: { source: { kind: "nacos", endpoint: "http://metrics-a" }, resource: {}, traffic: { httpRequestsTotal: 10, httpErrorsTotal: 1, httpDurationSecondsTotal: 1, httpDurationCount: 10 }, config: {}, naming: {} },
},
},
{
...sample(2_000, {}),
snapshot: {
...sample(2_000, {}).snapshot,
prometheus: { source: { kind: "nacos", endpoint: "http://metrics-b" }, resource: {}, traffic: { httpRequestsTotal: 100, httpErrorsTotal: 20, httpDurationSecondsTotal: 50, httpDurationCount: 100 }, config: {}, naming: {} },
},
},
];
expect(counterRateSeries(samples, (item) => item.snapshot.prometheus?.traffic.httpRequestsTotal)).toEqual([null, null]);
expect(
averageDurationMsSeries(
samples,
(item) => item.snapshot.prometheus?.traffic.httpDurationSecondsTotal,
(item) => item.snapshot.prometheus?.traffic.httpDurationCount,
),
).toEqual([null, null]);
expect(
errorRateSeries(
samples,
(item) => item.snapshot.prometheus?.traffic.httpErrorsTotal,
(item) => item.snapshot.prometheus?.traffic.httpRequestsTotal,
),
).toEqual([null, null]);
});
it("uses the private source fingerprint when redacted endpoints match", () => {
const samples: NacosDashboardSample[] = [
{
...sample(1_000, {}),
snapshot: {
...sample(1_000, {}).snapshot,
prometheus: { source: { kind: "nacos", endpoint: "http://metrics", fingerprint: "source-a" }, resource: {}, traffic: { httpRequestsTotal: 10 }, config: {}, naming: {} },
},
},
{
...sample(2_000, {}),
snapshot: {
...sample(2_000, {}).snapshot,
prometheus: { source: { kind: "nacos", endpoint: "http://metrics", fingerprint: "source-b" }, resource: {}, traffic: { httpRequestsTotal: 20 }, config: {}, naming: {} },
},
},
];
expect(counterRateSeries(samples, (item) => item.snapshot.prometheus?.traffic.httpRequestsTotal)).toEqual([null, null]);
});
it("calculates average latency and error percentage from counter deltas", () => {
const samples: NacosDashboardSample[] = [
{
...sample(1_000, {}),
snapshot: {
...sample(1_000, {}).snapshot,
prometheus: { source: { kind: "nacos", endpoint: "http://metrics" }, resource: {}, traffic: { httpRequestsTotal: 100, httpErrorsTotal: 2, httpDurationSecondsTotal: 10, httpDurationCount: 100 }, config: {}, naming: {} },
},
},
{
...sample(2_000, {}),
snapshot: {
...sample(2_000, {}).snapshot,
prometheus: { source: { kind: "nacos", endpoint: "http://metrics" }, resource: {}, traffic: { httpRequestsTotal: 120, httpErrorsTotal: 4, httpDurationSecondsTotal: 14, httpDurationCount: 120 }, config: {}, naming: {} },
},
},
];
expect(
averageDurationMsSeries(
samples,
(item) => item.snapshot.prometheus?.traffic.httpDurationSecondsTotal,
(item) => item.snapshot.prometheus?.traffic.httpDurationCount,
),
).toEqual([null, 200]);
expect(
errorRateSeries(
samples,
(item) => item.snapshot.prometheus?.traffic.httpErrorsTotal,
(item) => item.snapshot.prometheus?.traffic.httpRequestsTotal,
),
).toEqual([null, 10]);
});
it("keeps the newest samples within the requested limit", () => {
let samples: NacosDashboardSample[] = [];
for (let at = 1; at <= 4; at++) samples = appendDashboardSample(samples, sample(at, {}), 3);
expect(samples.map((item) => item.at)).toEqual([2, 3, 4]);
});
it("normalizes ratio and percentage resource values", () => {
expect(ratioPercent(0.25)).toBe(25);
expect(ratioPercent(25)).toBe(25);
expect(formatDashboardPercent(0.125)).toBe("12.5%");
expect(formatDashboardPercent(undefined)).toBe("—");
});
it("prefers the dashboard response namespace over the requested fallback", () => {
expect(dashboardNamespaceLabel("prod", undefined)).toBe("prod");
expect(dashboardNamespaceLabel("", "dev")).toBe("public");
expect(dashboardNamespaceLabel(undefined, "dev")).toBe("dev");
expect(dashboardNamespaceLabel(undefined, undefined)).toBe("public");
});
it("derives node health from explicit state when alive is absent", () => {
expect(isHealthyNacosNode({ state: "UP" })).toBe(true);
expect(isHealthyNacosNode({ state: "DOWN" })).toBe(false);
expect(isHealthyNacosNode({ alive: false, state: "UP" })).toBe(false);
});
});
+1
View File
@@ -345,6 +345,7 @@ export const nacosLoginRNacosConsole = forward("nacosLoginRNacosConsole");
export const nacosListServices = forward("nacosListServices");
export const nacosListInstances = forward("nacosListInstances");
export const nacosUpdateInstance = forward("nacosUpdateInstance");
export const nacosGetDashboard = forward("nacosGetDashboard");
export const nacosRawRequest = forward("nacosRawRequest");
// Data Transfer
+6
View File
@@ -166,6 +166,8 @@ import type {
NacosInstanceInfo,
NacosInstanceQuery,
NacosInstanceUpdate,
NacosDashboardQuery,
NacosDashboardSnapshot,
NacosNamespaceCreate,
NacosNamespaceInfo,
NacosNamespaceUpdate,
@@ -2256,6 +2258,10 @@ export async function nacosUpdateInstance(connectionId: string, req: NacosInstan
return post("/api/nacos/instances/update", { connectionId, req });
}
export async function nacosGetDashboard(connectionId: string, query: NacosDashboardQuery): Promise<NacosDashboardSnapshot> {
return post("/api/nacos/dashboard", { connectionId, query });
}
export async function nacosRawRequest(connectionId: string, req: NacosRawRequest): Promise<NacosRawResponse> {
return post("/api/nacos/raw", { connectionId, req });
}
@@ -17,6 +17,8 @@ import type {
NacosConfigRollbackRequest,
NacosConfigUpsert,
NacosConnectionInfo,
NacosDashboardQuery,
NacosDashboardSnapshot,
NacosRNacosConsoleCaptcha,
NacosInstanceInfo,
NacosInstanceQuery,
@@ -127,6 +129,10 @@ export async function nacosUpdateInstance(connectionId: string, req: NacosInstan
return invoke("nacos_update_instance", { connectionId, req });
}
export async function nacosGetDashboard(connectionId: string, query: NacosDashboardQuery): Promise<NacosDashboardSnapshot> {
return invoke("nacos_get_dashboard", { connectionId, query });
}
export async function nacosRawRequest(connectionId: string, req: NacosRawRequest): Promise<NacosRawResponse> {
return invoke("nacos_raw_request", { connectionId, req });
}
+23 -1
View File
@@ -91,9 +91,10 @@ export function normalizeNacosEndpoint(input: string, options: NacosEndpointNorm
contextPath = hasNacosSuffix ? rawPath : options.contextPath?.trim() || "/nacos";
} else if (detectedVersion === "v3" || hasNacos3UiSuffix) {
contextPath = rawPath.replace(/\/(?:next(?:\/index\.html)?|index\.html)$/i, "");
if (!contextPath) contextPath = options.contextPath?.trim() || "/nacos";
if (hasNacos3UiSuffix) warnings.push("The Nacos 3 console route was removed from the API context.");
} else if (!contextPath) {
contextPath = options.contextPath?.trim() || (detectedVersion === "v2" ? "/nacos" : "");
contextPath = options.contextPath?.trim() || (versionMode === "v2" ? "/nacos" : "");
}
url.pathname = "/";
url.search = "";
@@ -107,6 +108,27 @@ export function normalizeNacosEndpoint(input: string, options: NacosEndpointNorm
};
}
export function normalizeNacosMetricsUrl(input: string): string {
const value = input.trim();
let url: URL;
try {
url = new URL(value);
} catch {
throw new Error("Nacos Prometheus metrics URL must be a valid absolute URL");
}
if (!["http:", "https:"].includes(url.protocol)) throw new Error("Nacos Prometheus metrics URL must use HTTP or HTTPS");
if (url.username || url.password) throw new Error("Nacos Prometheus metrics URL must not contain embedded credentials");
if (value.includes("#")) throw new Error("Nacos Prometheus metrics URL must not contain a fragment");
return url.toString();
}
export function nacosMetricsCandidates(serverAddr: string, contextPath: string, implementation: NacosImplementation): string[] {
const base = serverAddr.replace(/\/+$/, "");
const context = contextPath.replace(/\/+$/, "");
const raw = implementation === "rnacos" ? [`${base}/metrics`, `${base}${context}/metrics`, `${base}/rnacos/metrics`] : [`${base}${context}/actuator/prometheus`, `${base}/nacos/actuator/prometheus`, `${base}/actuator/prometheus`];
return [...new Set(raw.map((value) => new URL(value).toString()))];
}
/**
* r-nacos exposes its Nacos-compatible OpenAPI on the service port (8848) at
* `/nacos`; `/rnacos` on 10848 is the separate web console and rejects these
@@ -0,0 +1,109 @@
import type { NacosDashboardMetrics, NacosDashboardSnapshot } from "@/types/nacos";
export const MAX_NACOS_DASHBOARD_SAMPLES = 60;
export interface NacosDashboardSample {
at: number;
snapshot: NacosDashboardSnapshot;
}
export type NacosDashboardMetricKey = keyof NacosDashboardMetrics;
export type NullableMetric = number | null;
export type NacosMetricSelector = (sample: NacosDashboardSample) => number | undefined;
export function dashboardNamespaceLabel(snapshotNamespace: string | undefined, requestedNamespace: string | undefined): string {
return (snapshotNamespace !== undefined ? snapshotNamespace : requestedNamespace) || "public";
}
export function hasContinuousPrometheusSource(previous: NacosDashboardSample, current: NacosDashboardSample): boolean {
const previousSource = previous.snapshot.prometheus?.source;
const currentSource = current.snapshot.prometheus?.source;
const previousIdentity = previousSource?.fingerprint ?? previousSource?.endpoint;
const currentIdentity = currentSource?.fingerprint ?? currentSource?.endpoint;
return !!previousSource && !!currentSource && previousSource.kind === currentSource.kind && previousIdentity === currentIdentity;
}
export function finiteMetric(value: number | undefined): NullableMetric {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
export function dashboardMetric(sample: NacosDashboardSample | undefined, key: NacosDashboardMetricKey): NullableMetric {
const value = sample?.snapshot.metrics?.[key];
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
export function dashboardSeries(samples: readonly NacosDashboardSample[], key: NacosDashboardMetricKey): NullableMetric[] {
return samples.map((sample) => dashboardMetric(sample, key));
}
export function gaugeSeries(samples: readonly NacosDashboardSample[], selector: NacosMetricSelector): NullableMetric[] {
return samples.map((sample) => finiteMetric(selector(sample)));
}
export function counterRateSeries(samples: readonly NacosDashboardSample[], selector: NacosMetricSelector): NullableMetric[] {
return samples.map((sample, index) => {
if (index === 0) return null;
const previousSample = samples[index - 1];
if (!hasContinuousPrometheusSource(previousSample, sample)) return null;
const previous = finiteMetric(selector(previousSample));
const current = finiteMetric(selector(sample));
const elapsedSeconds = (sample.at - samples[index - 1].at) / 1_000;
if (previous === null || current === null || current < previous || elapsedSeconds <= 0) return null;
return (current - previous) / elapsedSeconds;
});
}
export function averageDurationMsSeries(samples: readonly NacosDashboardSample[], sumSelector: NacosMetricSelector, countSelector: NacosMetricSelector): NullableMetric[] {
return samples.map((sample, index) => {
if (index === 0) return null;
const previousSample = samples[index - 1];
if (!hasContinuousPrometheusSource(previousSample, sample)) return null;
const previousSum = finiteMetric(sumSelector(previousSample));
const currentSum = finiteMetric(sumSelector(sample));
const previousCount = finiteMetric(countSelector(samples[index - 1]));
const currentCount = finiteMetric(countSelector(sample));
if (previousSum === null || currentSum === null || previousCount === null || currentCount === null) return null;
const sumDelta = currentSum - previousSum;
const countDelta = currentCount - previousCount;
if (sumDelta < 0 || countDelta <= 0) return null;
return (sumDelta / countDelta) * 1_000;
});
}
export function errorRateSeries(samples: readonly NacosDashboardSample[], errorSelector: NacosMetricSelector, requestSelector: NacosMetricSelector): NullableMetric[] {
return samples.map((sample, index) => {
if (index === 0) return null;
const previousSample = samples[index - 1];
if (!hasContinuousPrometheusSource(previousSample, sample)) return null;
const previousErrors = finiteMetric(errorSelector(previousSample));
const currentErrors = finiteMetric(errorSelector(sample));
const previousRequests = finiteMetric(requestSelector(samples[index - 1]));
const currentRequests = finiteMetric(requestSelector(sample));
if (previousErrors === null || currentErrors === null || previousRequests === null || currentRequests === null) return null;
const errorDelta = currentErrors - previousErrors;
const requestDelta = currentRequests - previousRequests;
if (errorDelta < 0 || requestDelta <= 0) return null;
return Math.min(100, Math.max(0, (errorDelta / requestDelta) * 100));
});
}
export function appendDashboardSample(samples: readonly NacosDashboardSample[], sample: NacosDashboardSample, maxSamples = MAX_NACOS_DASHBOARD_SAMPLES): NacosDashboardSample[] {
const next = [...samples, sample];
return next.length > maxSamples ? next.slice(next.length - maxSamples) : next;
}
export function ratioPercent(value: number | undefined): number | null {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
return Math.min(100, value <= 1 ? value * 100 : value);
}
export function formatDashboardPercent(value: number | undefined): string {
const percent = ratioPercent(value);
return percent === null ? "—" : `${percent.toFixed(1)}%`;
}
export function isHealthyNacosNode(node: { alive?: boolean; state?: string }): boolean {
if (typeof node.alive === "boolean") return node.alive;
return ["UP", "ONLINE", "HEALTHY"].includes(node.state?.trim().toUpperCase() ?? "");
}
@@ -5,6 +5,11 @@ describe("queryStore switchTab", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
vi.stubGlobal("localStorage", {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
});
setActivePinia(createPinia());
});
@@ -87,4 +92,16 @@ describe("queryStore switchTab", () => {
queryStore.updateDataGridLocalColumnFilters(tabId, {});
expect(tab.result.local_column_filters).toBeUndefined();
});
it("opens one reusable Nacos dashboard tab per connection", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
const tabId = queryStore.openNacosDashboard("nacos-1");
const reopenedTabId = queryStore.openNacosDashboard("nacos-1");
expect(reopenedTabId).toBe(tabId);
expect(queryStore.tabs.filter((tab) => tab.mode === "nacos-dashboard")).toHaveLength(1);
expect(queryStore.activeTabId).toBe(tabId);
});
});
+26
View File
@@ -1320,6 +1320,31 @@ export const useQueryStore = defineStore("query", () => {
return id;
}
function openNacosDashboard(connectionId: string) {
const existing = tabs.value.find((tab) => tab.mode === "nacos-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("serverDashboard.title")}` : t("serverDashboard.title"),
connectionId,
database: conn?.database || "",
sql: "",
isExecuting: false,
isCancelling: false,
isExplaining: false,
mode: "nacos-dashboard",
};
tabs.value.push(tab);
activeTabId.value = id;
return id;
}
function openDamengJobAdmin(connectionId: string) {
const existing = tabs.value.find((tab) => tab.mode === "dameng-jobs" && tab.connectionId === connectionId);
if (existing) {
@@ -4644,6 +4669,7 @@ export const useQueryStore = defineStore("query", () => {
openProcessList,
openMysqlDashboard,
openPostgresDashboard,
openNacosDashboard,
openDamengJobAdmin,
openMqAdmin,
openNacosAdmin,
+1 -1
View File
@@ -898,7 +898,7 @@ export interface QueryTab {
explainClientSessionId?: string;
/** Invalidates tab-scoped completion metadata after session context changes. */
completionContextVersion?: number;
mode: "data" | "query" | "redis" | "redis-dashboard" | "mongo" | "mongo-gridfs" | "mongo-bucket" | "vector" | "etcd" | "zookeeper" | "mq" | "nacos" | "objects" | "structure" | "users" | "dameng-jobs" | "processlist" | "mysql-dashboard" | "postgres-dashboard";
mode: "data" | "query" | "redis" | "redis-dashboard" | "mongo" | "mongo-gridfs" | "mongo-bucket" | "vector" | "etcd" | "zookeeper" | "mq" | "nacos" | "nacos-dashboard" | "objects" | "structure" | "users" | "dameng-jobs" | "processlist" | "mysql-dashboard" | "postgres-dashboard";
mqTenant?: string;
mqInitialTab?: "topics";
nacosNamespace?: string;
+121
View File
@@ -51,6 +51,7 @@ export interface NacosAuthConfig {
export type NacosImplementation = "nacos" | "rnacos";
export type NacosVersionMode = "auto" | "v2" | "v3";
export type NacosMetricsMode = "auto" | "disabled" | "custom";
export type NacosRNacosConsoleAuth = { kind: "inherit" } | { kind: "usernamePassword"; username: string; password: string };
export interface NacosAdminConfig {
@@ -65,6 +66,8 @@ export interface NacosAdminConfig {
rnacosConsoleAuth?: NacosRNacosConsoleAuth;
auth?: NacosAuthConfig;
tlsSkipVerify?: boolean;
metricsMode?: NacosMetricsMode;
metricsUrl?: string;
pageSize?: number;
}
@@ -344,6 +347,124 @@ export interface NacosInstanceUpdate {
metadata?: unknown;
}
export interface NacosDashboardQuery {
namespace?: string;
}
export interface NacosDashboardMetrics {
status?: string;
serviceCount?: number;
instanceCount?: number;
subscribeCount?: number;
raftNotifyTaskCount?: number;
responsibleServiceCount?: number;
responsibleInstanceCount?: number;
clientCount?: number;
connectionBasedClientCount?: number;
ephemeralIpPortClientCount?: number;
persistentIpPortClientCount?: number;
responsibleClientCount?: number;
cpu?: number;
load?: number;
mem?: number;
}
export interface NacosPrometheusSource {
kind: NacosImplementation;
endpoint: string;
fingerprint?: string;
}
export interface NacosPrometheusResourceMetrics {
cpuRatio?: number;
memoryRatio?: number;
memoryUsedBytes?: number;
memoryMaxBytes?: number;
rssBytes?: number;
vmsBytes?: number;
systemTotalMemoryBytes?: number;
load1m?: number;
jvmDaemonThreads?: number;
gcPauseCount?: number;
}
export interface NacosPrometheusTrafficMetrics {
httpRequestsTotal?: number;
grpcRequestsTotal?: number;
httpErrorsTotal?: number;
grpcErrorsTotal?: number;
httpDurationSecondsTotal?: number;
httpDurationCount?: number;
grpcDurationSecondsTotal?: number;
grpcDurationCount?: number;
httpP50Ms?: number;
httpP95Ms?: number;
httpP99Ms?: number;
grpcP50Ms?: number;
grpcP95Ms?: number;
grpcP99Ms?: number;
executorPoolSize?: number;
executorActiveCount?: number;
executorQueuedTasks?: number;
}
export interface NacosPrometheusConfigMetrics {
configCount?: number;
getConfigTotal?: number;
publishTotal?: number;
longPolling?: number;
listenerClients?: number;
listenerKeys?: number;
notifyTasks?: number;
notifyClientTasks?: number;
dumpTasks?: number;
subscriberCount?: number;
}
export interface NacosPrometheusNamingMetrics {
serviceCount?: number;
instanceCount?: number;
subscriberCount?: number;
connectionCount?: number;
totalPush?: number;
failedPush?: number;
emptyPush?: number;
pushPendingTasks?: number;
avgPushCostMs?: number;
maxPushCostMs?: number;
leaderStatus?: number;
}
export interface NacosPrometheusSnapshot {
source: NacosPrometheusSource;
resource: NacosPrometheusResourceMetrics;
traffic: NacosPrometheusTrafficMetrics;
config: NacosPrometheusConfigMetrics;
naming: NacosPrometheusNamingMetrics;
}
export interface NacosClusterNode {
address: string;
ip?: string;
port?: number;
state?: string;
alive?: boolean;
site?: string;
weight?: number;
lastRefreshTime?: string;
}
export interface NacosDashboardSnapshot {
namespace: string;
namespaceCount?: number;
configCount?: number;
serviceCount?: number;
metrics?: NacosDashboardMetrics;
prometheus?: NacosPrometheusSnapshot;
nodes: NacosClusterNode[];
warnings: string[];
}
export interface NacosRawRequest {
method: string;
path: string;