feat(nacos): improve service and instance management
This commit is contained in:
parent
b5ba730404
commit
c754b85295
|
|
@ -19,7 +19,7 @@ import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb";
|
|||
import type { VictoriaMetricsExternalConfig } from "@/types/victoriametrics";
|
||||
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
|
||||
import type { MqttConnectionConfig } from "@/types/mqtt";
|
||||
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
|
||||
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosNamespaceInfo, 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";
|
||||
|
|
@ -64,7 +64,8 @@ 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 { nacosMetricsCandidates, normalizeNacosEndpoint, normalizeNacosMetricsUrl } from "@/lib/nacos/nacosAdmin";
|
||||
import { normalizeNacosEndpoint, normalizeNacosMetricsUrl } from "@/lib/nacos/nacosAdmin";
|
||||
import { normalizeNacosNamespaceSelection, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowDown,
|
||||
|
|
@ -115,6 +116,7 @@ export type ConfigTab = "connection" | "advanced" | "tls" | "transport";
|
|||
type ProductionScope = "connection" | "databases";
|
||||
type MqTokenSigningMode = "none" | "hs256" | "rs256";
|
||||
type NacosAuthKind = NacosAuthConfig["kind"];
|
||||
type NacosConnectionProfile = "v2" | "v3" | "rnacos";
|
||||
type DremioConnectionMode = "arrow-flight-sql" | "legacy";
|
||||
type JdbcDriverSelectItem = {
|
||||
id: string;
|
||||
|
|
@ -128,6 +130,11 @@ const DREMIO_ARROW_FLIGHT_SQL_JDBC_DRIVER_CLASS = "org.apache.arrow.driver.jdbc.
|
|||
const DREMIO_LEGACY_JDBC_URL = "jdbc:dremio:direct=127.0.0.1:31010";
|
||||
const DREMIO_LEGACY_JDBC_DRIVER_CLASS = "com.dremio.jdbc.Driver";
|
||||
const DEFAULT_SSH_USER = "root";
|
||||
const NACOS_CONNECTION_PROFILES: ReadonlyArray<{ value: NacosConnectionProfile; title: string }> = [
|
||||
{ value: "v2", title: "Nacos 2.x" },
|
||||
{ value: "v3", title: "Nacos 3.x" },
|
||||
{ value: "rnacos", title: "r-nacos" },
|
||||
];
|
||||
|
||||
type LegacyTransportFields = {
|
||||
ssh_enabled?: boolean;
|
||||
|
|
@ -150,7 +157,6 @@ type LegacyTransportFields = {
|
|||
type LegacyConnectionConfig = ConnectionConfig & LegacyTransportFields;
|
||||
type ConnectionForm = Omit<ConnectionConfig, "id">;
|
||||
type ConnectionTestState = ConnectionTestResult & { ok: boolean };
|
||||
type SuccessfulConnectionTest = { result: ConnectionTestResult; config: ConnectionConfig };
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
|
|
@ -201,6 +207,12 @@ const visibleDatabaseSelection = ref<Set<string>>(new Set());
|
|||
const visibleDatabaseSearchText = ref("");
|
||||
const visibleDatabaseError = ref("");
|
||||
const visibleDatabaseShowSystem = ref(false);
|
||||
const showVisibleNacosNamespacesDialog = ref(false);
|
||||
const isLoadingVisibleNacosNamespaces = ref(false);
|
||||
const visibleNacosNamespaces = ref<NacosNamespaceInfo[]>([]);
|
||||
const visibleNacosNamespaceSelection = ref<Set<string>>(new Set());
|
||||
const visibleNacosNamespaceSearchText = ref("");
|
||||
const visibleNacosNamespaceError = ref("");
|
||||
const showProductionDatabasesDialog = ref(false);
|
||||
const isLoadingProductionDatabases = ref(false);
|
||||
const productionDatabaseNames = ref<string[]>([]);
|
||||
|
|
@ -673,11 +685,11 @@ const mqKafkaSaslMechanismOptions = [
|
|||
{ value: "SCRAM-SHA-512", label: "SCRAM-SHA-512" },
|
||||
];
|
||||
const nacosImplementation = ref<NacosImplementation>("nacos");
|
||||
const nacosVersionMode = ref<NacosVersionMode>("auto");
|
||||
// Nacos 2 and 3 expose different API planes (and Nacos 3 commonly needs a
|
||||
// separate Console address). New connections must therefore choose an
|
||||
// explicit version instead of relying on endpoint-shape guessing.
|
||||
const nacosVersionMode = ref<NacosVersionMode>("v2");
|
||||
const nacosServerAddr = ref("");
|
||||
const nacosNamespace = ref("");
|
||||
const nacosContextPath = ref("");
|
||||
const nacosContextPathCustomized = ref(false);
|
||||
const nacosRNacosConsoleAddr = ref("");
|
||||
const nacosHistoryEnabled = ref(false);
|
||||
const nacosConsoleAuthKind = ref<NacosRNacosConsoleAuth["kind"]>("inherit");
|
||||
|
|
@ -706,63 +718,20 @@ const mqttTlsSkipVerify = ref(false);
|
|||
const mqttKeepAliveSecs = ref(60);
|
||||
const mqttConnectTimeoutSecs = ref(30);
|
||||
const mqttMaxPacketSizeBytes = ref(16 * 1024 * 1024);
|
||||
|
||||
const nacosPrimaryAddressLabel = computed(() => {
|
||||
if (nacosImplementation.value === "rnacos") return t("connection.nacosPrimaryAddressRNacos");
|
||||
if (nacosVersionMode.value === "v2") return t("connection.nacosPrimaryAddressV2");
|
||||
if (nacosVersionMode.value === "v3") return t("connection.nacosPrimaryAddressV3");
|
||||
return t("connection.nacosPrimaryAddressAuto");
|
||||
});
|
||||
const nacosPrimaryAddressPlaceholder = computed(() => {
|
||||
return "http://127.0.0.1:8848/nacos";
|
||||
});
|
||||
const nacosNormalizedPreview = computed(() => {
|
||||
if (!nacosServerAddr.value.trim()) return "";
|
||||
const nacosV3AdminEndpointWarning = computed(() => {
|
||||
if (nacosImplementation.value !== "nacos" || nacosVersionMode.value !== "v3" || !nacosServerAddr.value.trim()) return "";
|
||||
try {
|
||||
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
return `${normalized.serverAddr}${normalized.contextPath || ""}`;
|
||||
const url = new URL(nacosServerAddr.value.trim());
|
||||
if (url.port === "8080") {
|
||||
return t("nacos.nacosV3ConsolePortWarning");
|
||||
}
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
const nacosEffectiveContextPath = computed(() => {
|
||||
if (!nacosServerAddr.value.trim()) {
|
||||
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : "/nacos";
|
||||
}
|
||||
try {
|
||||
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
return normalized.contextPath || "/";
|
||||
} catch {
|
||||
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : "/";
|
||||
}
|
||||
});
|
||||
const nacosContextPathInput = computed({
|
||||
get: () => (nacosContextPathCustomized.value ? nacosContextPath.value : nacosEffectiveContextPath.value),
|
||||
set: (value: string) => {
|
||||
nacosContextPathCustomized.value = true;
|
||||
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 "";
|
||||
// The normal form validation will report an invalid URL on save.
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const nacosMetricsUrlError = computed(() => {
|
||||
if (nacosMetricsMode.value !== "custom") return "";
|
||||
|
|
@ -774,13 +743,22 @@ const nacosMetricsUrlError = computed(() => {
|
|||
}
|
||||
});
|
||||
|
||||
function resetNacosContextPathCustomization() {
|
||||
nacosContextPathCustomized.value = false;
|
||||
nacosContextPath.value = "";
|
||||
const nacosConnectionProfile = computed<NacosConnectionProfile>(() => {
|
||||
if (nacosImplementation.value === "rnacos") return "rnacos";
|
||||
return nacosVersionMode.value === "v3" ? "v3" : "v2";
|
||||
});
|
||||
|
||||
function selectNacosConnectionProfile(profile: NacosConnectionProfile) {
|
||||
if (profile === "rnacos") {
|
||||
nacosImplementation.value = "rnacos";
|
||||
return;
|
||||
}
|
||||
nacosImplementation.value = "nacos";
|
||||
nacosVersionMode.value = profile;
|
||||
}
|
||||
|
||||
watch(nacosImplementation, (implementation) => {
|
||||
if (implementation === "rnacos") nacosVersionMode.value = "auto";
|
||||
if (implementation === "rnacos") nacosVersionMode.value = "v2";
|
||||
if (implementation !== "rnacos") nacosHistoryEnabled.value = false;
|
||||
});
|
||||
|
||||
|
|
@ -1211,11 +1189,12 @@ watch(mqAuthKind, (kind) => {
|
|||
|
||||
function resetNacosFields(config?: Partial<NacosAdminConfig>) {
|
||||
nacosImplementation.value = config?.implementation || (config?.rnacosConsoleAddr ? "rnacos" : "nacos");
|
||||
nacosVersionMode.value = config?.versionMode || "auto";
|
||||
nacosServerAddr.value = config?.serverAddr?.trim() || "";
|
||||
nacosNamespace.value = config?.namespace || "";
|
||||
nacosContextPath.value = config?.contextPath || "";
|
||||
nacosContextPathCustomized.value = !!config?.contextPath;
|
||||
// Saved `auto` profiles are legacy records. The form always saves an
|
||||
// explicit selection and no longer relies on a separate Console address.
|
||||
nacosVersionMode.value = config?.versionMode === "v3" ? "v3" : "v2";
|
||||
const serverAddr = config?.serverAddr?.trim() || "";
|
||||
const contextPath = config?.contextPath?.trim() || "";
|
||||
nacosServerAddr.value = serverAddr && contextPath && contextPath !== "/" && !serverAddr.endsWith(contextPath) ? `${serverAddr.replace(/\/+$/, "")}/${contextPath.replace(/^\/+/, "")}` : serverAddr;
|
||||
nacosRNacosConsoleAddr.value = config?.rnacosConsoleAddr?.trim() || "";
|
||||
nacosHistoryEnabled.value = config?.rnacosHistoryEnabled ?? !!config?.rnacosConsoleAddr;
|
||||
const consoleAuth = config?.rnacosConsoleAuth || { kind: "inherit" };
|
||||
|
|
@ -1507,13 +1486,14 @@ function buildNacosAdminConfig(): NacosAdminConfig {
|
|||
const normalized = normalizeNacosEndpoint(primaryAddress, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
contextPath: undefined,
|
||||
});
|
||||
if (nacosImplementation.value === "rnacos" && normalized.warnings.length) {
|
||||
throw new Error(t("connection.nacosRNacosOpenApiRequired"));
|
||||
}
|
||||
const rnacosConsoleConfigured = nacosImplementation.value === "rnacos" && !!nacosRNacosConsoleAddr.value.trim();
|
||||
if (nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value && !rnacosConsoleConfigured) {
|
||||
const rnacosExtensionsEnabled = nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value;
|
||||
const rnacosConsoleConfigured = rnacosExtensionsEnabled && !!nacosRNacosConsoleAddr.value.trim();
|
||||
if (rnacosExtensionsEnabled && !rnacosConsoleConfigured) {
|
||||
throw new Error(t("connection.nacosRNacosConsoleUrlRequired"));
|
||||
}
|
||||
let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined;
|
||||
|
|
@ -1541,9 +1521,8 @@ function buildNacosAdminConfig(): NacosAdminConfig {
|
|||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosImplementation.value === "nacos" ? nacosVersionMode.value : undefined,
|
||||
serverAddr: normalized.serverAddr,
|
||||
namespace: nacosNamespace.value.trim() || undefined,
|
||||
contextPath: normalized.contextPath || undefined,
|
||||
rnacosConsoleAddr: nacosImplementation.value === "rnacos" ? nacosRNacosConsoleAddr.value.trim() || undefined : undefined,
|
||||
rnacosConsoleAddr: rnacosExtensionsEnabled ? nacosRNacosConsoleAddr.value.trim() || undefined : undefined,
|
||||
rnacosHistoryEnabled: nacosImplementation.value === "rnacos" ? nacosHistoryEnabled.value : undefined,
|
||||
rnacosConsoleAuth,
|
||||
auth: buildNacosAuth(),
|
||||
|
|
@ -1554,49 +1533,6 @@ function buildNacosAdminConfig(): NacosAdminConfig {
|
|||
};
|
||||
}
|
||||
|
||||
function dockerNacosConsoleFallbackUrl(serverAddr: string): string | null {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(serverAddr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (port !== "8848" || !["127.0.0.1", "localhost", "::1"].includes(host)) return null;
|
||||
|
||||
parsed.port = "8085";
|
||||
return parsed.toString().replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function isNacosAdminEndpointNotFound(message: string): boolean {
|
||||
return /Nacos admin endpoint was not found/i.test(message);
|
||||
}
|
||||
|
||||
async function tryNacosDockerConsoleFallback(config: ConnectionConfig, originalError: string, runId: number): Promise<SuccessfulConnectionTest | null> {
|
||||
if (config.db_type !== "nacos" || !isNacosAdminEndpointNotFound(originalError)) return null;
|
||||
|
||||
const fallbackUrl = dockerNacosConsoleFallbackUrl(nacosServerAddr.value);
|
||||
if (!fallbackUrl || fallbackUrl === nacosServerAddr.value.trim()) return null;
|
||||
|
||||
const previousUrl = nacosServerAddr.value;
|
||||
nacosServerAddr.value = fallbackUrl;
|
||||
try {
|
||||
const fallbackConfig = connectionConfigForSubmit(config.id, config.name);
|
||||
const result = await testConnectionWithTimeout(fallbackConfig, runId);
|
||||
return {
|
||||
config: fallbackConfig,
|
||||
result: {
|
||||
...result,
|
||||
message: `${result.message} ${t("connection.nacosConsoleUrlAutoAdjusted", { from: previousUrl.trim(), to: fallbackUrl })}`,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
nacosServerAddr.value = previousUrl;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
|
|
@ -2858,7 +2794,8 @@ const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" &&
|
|||
const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile));
|
||||
const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true);
|
||||
const agentDriverFocus = computed<DriverStoreFocus>(() => ({ target: "driver", driver: agentDriverInstallKey(form.value.db_type, form.value.driver_profile) }));
|
||||
const canChooseVisibleDatabases = computed(() => connectionCanChooseVisibleDatabases(form.value));
|
||||
const canChooseVisibleNacosNamespaces = computed(() => form.value.db_type === "nacos");
|
||||
const canChooseVisibleDatabases = computed(() => !canChooseVisibleNacosNamespaces.value && connectionCanChooseVisibleDatabases(form.value));
|
||||
const visibleFilterUsesSchemas = computed(() => connectionUsesVisibleSchemaFilter(form.value));
|
||||
const hasVisibleDatabaseFilter = computed(() => Array.isArray(form.value.visible_databases));
|
||||
const visibleDatabaseSummary = computed(() => {
|
||||
|
|
@ -2882,6 +2819,16 @@ const visibleDatabaseTotalCount = computed(() => listedVisibleDatabaseNames.valu
|
|||
const visibleDatabaseCanSave = computed(() => canSaveVisibleDatabaseSelection([...visibleDatabaseSelection.value]));
|
||||
const visibleDatabaseHasSystemObjects = computed(() => defaultListedVisibleDatabaseNames.value.length < visibleDatabaseNames.value.length);
|
||||
const visibleSystemObjectsLabelKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.showSystemSchemas" : "visibleDatabases.showSystemDatabases"));
|
||||
const filteredVisibleNacosNamespaces = computed(() => {
|
||||
const query = visibleNacosNamespaceSearchText.value.trim().toLowerCase();
|
||||
if (!query) return visibleNacosNamespaces.value;
|
||||
return visibleNacosNamespaces.value.filter((namespace) => {
|
||||
const label = namespace.namespaceShowName || namespace.namespace || "public";
|
||||
return `${label} ${namespace.namespace}`.toLowerCase().includes(query);
|
||||
});
|
||||
});
|
||||
const visibleNacosNamespaceSelectedCount = computed(() => visibleNacosNamespaceSelection.value.size);
|
||||
const visibleNacosNamespaceCanSave = computed(() => visibleNacosNamespaceSelection.value.size > 0);
|
||||
const filteredProductionDatabaseNames = computed(() => {
|
||||
const query = productionDatabaseSearchText.value.trim().toLowerCase();
|
||||
if (!query) return productionDatabaseNames.value;
|
||||
|
|
@ -3028,7 +2975,7 @@ const agentInstallProgressLabel = computed(() => {
|
|||
});
|
||||
const canCloseAgentInstallDialog = computed(() => !agentInstallRunning.value || !!agentInstallError.value);
|
||||
const sqlServerDriverMode = computed<"auto" | "legacy">(() => (sqlServerUsesLegacyCompatibility(form.value) ? "legacy" : "auto"));
|
||||
const shouldUseWideConnectionDialog = computed(() => dialogStep.value === "config" && (canChooseVisibleDatabases.value || (canChooseVisibleSchemas.value && !visibleFilterUsesSchemas.value)));
|
||||
const shouldUseWideConnectionDialog = computed(() => dialogStep.value === "config" && (canChooseVisibleDatabases.value || canChooseVisibleNacosNamespaces.value || (canChooseVisibleSchemas.value && !visibleFilterUsesSchemas.value)));
|
||||
const connectionDialogContentClass = computed(() => {
|
||||
if (dialogStep.value === "select") return "connection-dialog-content--picker sm:h-[720px] sm:max-w-[880px]";
|
||||
const widthClass = shouldUseWideConnectionDialog.value ? "connection-dialog-content--wide sm:max-w-[660px]" : "connection-dialog-content--standard sm:max-w-[560px]";
|
||||
|
|
@ -3147,21 +3094,13 @@ async function testConnection() {
|
|||
if (runId !== testRunId) return;
|
||||
const rawMessage = mongodbAuthFailureHint(errorMessage(e));
|
||||
const message = config ? connectionErrorWithDriverUpdateHint(config, rawMessage) : rawMessage;
|
||||
const fallback = config ? await tryNacosDockerConsoleFallback(config, message, runId) : null;
|
||||
if (runId !== testRunId) return;
|
||||
if (fallback) {
|
||||
applySuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName);
|
||||
void persistSuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName, runId);
|
||||
clearEditedConnectionErrorAfterSuccessfulTest();
|
||||
} else {
|
||||
const shouldShowSqlServerLegacyMode = config?.db_type === "sqlserver" && !sqlServerUsesLegacyCompatibility(config) && isSqlServerTlsHandshakeFailure(message);
|
||||
if (shouldShowSqlServerLegacyMode) {
|
||||
configTab.value = "advanced";
|
||||
}
|
||||
clearTestedConnectionInfo();
|
||||
testResult.value = { ok: false, message };
|
||||
showConnectionError(message);
|
||||
const shouldShowSqlServerLegacyMode = config?.db_type === "sqlserver" && !sqlServerUsesLegacyCompatibility(config) && isSqlServerTlsHandshakeFailure(message);
|
||||
if (shouldShowSqlServerLegacyMode) {
|
||||
configTab.value = "advanced";
|
||||
}
|
||||
clearTestedConnectionInfo();
|
||||
testResult.value = { ok: false, message };
|
||||
showConnectionError(message);
|
||||
} finally {
|
||||
if (runId === testRunId) {
|
||||
isTesting.value = false;
|
||||
|
|
@ -3452,7 +3391,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
|||
applyNacosServerAddr(config, nacosConfig.serverAddr);
|
||||
config.username = nacosAuthKind.value === "usernamePassword" ? nacosUsername.value.trim() : "";
|
||||
config.password = nacosAuthKind.value === "usernamePassword" ? nacosPassword.value : "";
|
||||
config.database = nacosConfig.namespace || undefined;
|
||||
config.database = undefined;
|
||||
config.connection_string = undefined;
|
||||
config.url_params = "";
|
||||
} else if (config.db_type === "mqtt") {
|
||||
|
|
@ -3898,6 +3837,15 @@ function resetVisibleDatabaseDraftState() {
|
|||
visibleDatabaseShowSystem.value = false;
|
||||
}
|
||||
|
||||
function resetVisibleNacosNamespaceDraftState() {
|
||||
showVisibleNacosNamespacesDialog.value = false;
|
||||
isLoadingVisibleNacosNamespaces.value = false;
|
||||
visibleNacosNamespaces.value = [];
|
||||
visibleNacosNamespaceSelection.value = new Set();
|
||||
visibleNacosNamespaceSearchText.value = "";
|
||||
visibleNacosNamespaceError.value = "";
|
||||
}
|
||||
|
||||
function resetProductionDatabaseDraftState() {
|
||||
showProductionDatabasesDialog.value = false;
|
||||
isLoadingProductionDatabases.value = false;
|
||||
|
|
@ -3967,6 +3915,78 @@ async function openVisibleDatabasesPicker() {
|
|||
}
|
||||
}
|
||||
|
||||
function nacosNamespaceValue(namespace: NacosNamespaceInfo): string {
|
||||
return namespace.namespace || "";
|
||||
}
|
||||
|
||||
function nacosNamespaceLabel(namespace: NacosNamespaceInfo): string {
|
||||
return namespace.namespaceShowName || namespace.namespace || "public";
|
||||
}
|
||||
|
||||
function normalizeVisibleNacosNamespaceSelection(selected: Iterable<string>, namespaces: NacosNamespaceInfo[]): string[] {
|
||||
return normalizeNacosNamespaceSelection(selected, namespaces);
|
||||
}
|
||||
|
||||
async function openVisibleNacosNamespacesPicker() {
|
||||
if (!ensureConnectionHostResolvedFromUrl()) return;
|
||||
if (!canChooseVisibleNacosNamespaces.value || isLoadingVisibleNacosNamespaces.value) return;
|
||||
|
||||
isLoadingVisibleNacosNamespaces.value = true;
|
||||
visibleNacosNamespaceError.value = "";
|
||||
visibleNacosNamespaceSearchText.value = "";
|
||||
const draftId = buildDraftVisibleDatabasesConnectionId(uuid());
|
||||
|
||||
try {
|
||||
const draftConfig = {
|
||||
...connectionConfigForSubmit(draftId),
|
||||
id: draftId,
|
||||
one_time: true,
|
||||
};
|
||||
await api.connectDb(draftConfig);
|
||||
const namespaces = normalizeNacosNamespacesForDisplay(await api.nacosListNamespaces(draftId));
|
||||
visibleNacosNamespaces.value = [...namespaces].sort((left, right) => nacosNamespaceLabel(left).localeCompare(nacosNamespaceLabel(right)));
|
||||
const configured = form.value.visible_databases;
|
||||
const initialSelection = Array.isArray(configured) ? normalizeVisibleNacosNamespaceSelection(configured, visibleNacosNamespaces.value) : visibleNacosNamespaces.value.map(nacosNamespaceValue);
|
||||
visibleNacosNamespaceSelection.value = new Set(initialSelection);
|
||||
showVisibleNacosNamespacesDialog.value = true;
|
||||
} catch (e: any) {
|
||||
visibleNacosNamespaces.value = [];
|
||||
visibleNacosNamespaceSelection.value = new Set();
|
||||
visibleNacosNamespaceError.value = errorMessage(e);
|
||||
testResult.value = { ok: false, message: visibleNacosNamespaceError.value };
|
||||
showVisibleNacosNamespacesDialog.value = true;
|
||||
} finally {
|
||||
await api.disconnectDb(draftId).catch(() => undefined);
|
||||
isLoadingVisibleNacosNamespaces.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVisibleNacosNamespace(namespace: string) {
|
||||
const next = new Set(visibleNacosNamespaceSelection.value);
|
||||
if (next.has(namespace)) next.delete(namespace);
|
||||
else next.add(namespace);
|
||||
visibleNacosNamespaceSelection.value = next;
|
||||
}
|
||||
|
||||
function selectAllVisibleNacosNamespaces() {
|
||||
visibleNacosNamespaceSelection.value = new Set(visibleNacosNamespaces.value.map(nacosNamespaceValue));
|
||||
}
|
||||
|
||||
function clearVisibleNacosNamespaceSelection() {
|
||||
visibleNacosNamespaceSelection.value = new Set();
|
||||
}
|
||||
|
||||
function showAllVisibleNacosNamespaces() {
|
||||
form.value.visible_databases = undefined;
|
||||
resetVisibleNacosNamespaceDraftState();
|
||||
}
|
||||
|
||||
function saveVisibleNacosNamespaceSelection() {
|
||||
if (!visibleNacosNamespaceCanSave.value) return;
|
||||
form.value.visible_databases = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaceSelection.value, visibleNacosNamespaces.value);
|
||||
showVisibleNacosNamespacesDialog.value = false;
|
||||
}
|
||||
|
||||
async function loadVisibleDatabaseNames(connectionId: string, config: ConnectionConfig): Promise<string[]> {
|
||||
if (connectionUsesVisibleSchemaFilter(config)) {
|
||||
return api.listSchemas(connectionId, config.database || "");
|
||||
|
|
@ -4233,6 +4253,7 @@ function resetForm() {
|
|||
selectedDbCategory.value = "sql";
|
||||
configTab.value = "connection";
|
||||
resetVisibleDatabaseDraftState();
|
||||
resetVisibleNacosNamespaceDraftState();
|
||||
resetProductionDatabaseDraftState();
|
||||
resetVisibleSchemasState();
|
||||
resetTestState();
|
||||
|
|
@ -5085,7 +5106,7 @@ function openExternalUrl(url: string) {
|
|||
</div>
|
||||
|
||||
<TabsContent value="connection" class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto pt-4 pr-2 pb-2">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto pt-4 pr-2 pb-6" :class="{ 'connection-form-body--nacos': form.db_type === 'nacos' }">
|
||||
<div v-if="!isJdbcConnection && form.db_type !== 'nacos'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.connectionUrlOptional") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
|
|
@ -5654,146 +5675,74 @@ function openExternalUrl(url: string) {
|
|||
|
||||
<!-- Nacos: profile-aware endpoint, namespace and auth -->
|
||||
<template v-else-if="form.db_type === 'nacos'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosImplementation") }}</Label>
|
||||
<div class="col-span-3 flex gap-2">
|
||||
<Button size="sm" :variant="nacosImplementation === 'nacos' ? 'default' : 'outline'" @click="nacosImplementation = 'nacos'">Nacos</Button>
|
||||
<Button size="sm" :variant="nacosImplementation === 'rnacos' ? 'default' : 'outline'" @click="nacosImplementation = 'rnacos'">r-nacos</Button>
|
||||
<section data-nacos-profile-selector class="overflow-hidden rounded-lg border bg-muted/10">
|
||||
<div class="border-b px-4 py-3">
|
||||
<div class="text-sm font-medium">{{ t("nacos.nacosConnectionPlan") }}</div>
|
||||
<p class="mt-0.5 text-xs leading-5 text-muted-foreground">{{ t("nacos.nacosConnectionPlanDescription") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nacosImplementation === 'nacos'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosVersion") }}</Label>
|
||||
<div class="col-span-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" :variant="nacosVersionMode === 'auto' ? 'default' : 'outline'" @click="nacosVersionMode = 'auto'">{{ t("connection.nacosVersionAuto") }}</Button>
|
||||
<Button size="sm" :variant="nacosVersionMode === 'v2' ? 'default' : 'outline'" @click="nacosVersionMode = 'v2'">2.x</Button>
|
||||
<Button size="sm" :variant="nacosVersionMode === 'v3' ? 'default' : 'outline'" @click="nacosVersionMode = 'v3'">3.x</Button>
|
||||
<div class="grid grid-cols-3 gap-2 p-3">
|
||||
<button
|
||||
v-for="profile in NACOS_CONNECTION_PROFILES"
|
||||
:key="profile.value"
|
||||
type="button"
|
||||
class="min-w-0 rounded-md border px-3 py-2.5 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
:class="nacosConnectionProfile === profile.value ? 'border-primary bg-primary/5 shadow-sm' : 'border-border bg-background'"
|
||||
:aria-pressed="nacosConnectionProfile === profile.value"
|
||||
@click="selectNacosConnectionProfile(profile.value)"
|
||||
>
|
||||
<span class="block truncate text-sm font-medium">{{ profile.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ nacosPrimaryAddressLabel }}</Label>
|
||||
<Input v-model="nacosServerAddr" class="col-span-3" :placeholder="nacosPrimaryAddressPlaceholder" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosPrimaryAddressHint") }}</p>
|
||||
</div>
|
||||
<div v-if="nacosNormalizedPreview" class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosRequestsUse", { address: nacosNormalizedPreview }) }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosContextPath") }}</Label>
|
||||
<div class="col-span-3 flex min-w-0 items-center gap-2">
|
||||
<Input v-model="nacosContextPathInput" class="min-w-0 flex-1" :placeholder="t('connection.nacosContextPathPlaceholder')" />
|
||||
<Button v-if="nacosContextPathCustomized" type="button" size="sm" variant="ghost" @click="resetNacosContextPathCustomization">{{ t("connection.nacosContextPathRestoreAuto") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<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>
|
||||
<div class="col-span-3 flex items-center gap-2">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<Switch v-model="nacosHistoryEnabled" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.nacosConfigurationHistoryEnable") }}</span>
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" class="max-w-[360px] text-xs leading-relaxed">
|
||||
{{ t("connection.nacosConfigurationHistoryHint") }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</section>
|
||||
|
||||
<section data-nacos-endpoint-section class="rounded-lg border p-4">
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("nacos.nacosServiceAddress") }}</Label>
|
||||
<Input v-model="nacosServerAddr" :placeholder="nacosPrimaryAddressPlaceholder" />
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
<template>{{ t("nacos.nacosServiceAddressHint") }}</template>
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="nacosV3AdminEndpointWarning" class="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs leading-5 text-amber-700 dark:text-amber-400">
|
||||
{{ nacosV3AdminEndpointWarning }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosRNacosConsoleUrl") }}</Label>
|
||||
<Input v-model="nacosRNacosConsoleAddr" class="col-span-3" :placeholder="t('connection.nacosRNacosConsoleUrlPlaceholder')" />
|
||||
</section>
|
||||
|
||||
<section data-nacos-access-section class="rounded-lg border p-4">
|
||||
<div class="mb-4">
|
||||
<div class="text-sm font-medium">{{ t("nacos.nacosAuth") }}</div>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">{{ t("nacos.nacosAuthHint") }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosRNacosConsoleUrlHint") }}</p>
|
||||
</div>
|
||||
<template v-if="nacosRNacosConsoleAddr.trim()">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleAuthentication") }}</Label>
|
||||
<div class="col-span-3 flex gap-2">
|
||||
<Button size="sm" :variant="nacosConsoleAuthKind === 'inherit' ? 'default' : 'outline'" :disabled="nacosAuthKind === 'none'" @click="nacosConsoleAuthKind = 'inherit'">{{ t("connection.nacosConsoleAuthInherit") }}</Button>
|
||||
<Button size="sm" :variant="nacosConsoleAuthKind === 'usernamePassword' ? 'default' : 'outline'" @click="nacosConsoleAuthKind = 'usernamePassword'">{{ t("connection.nacosConsoleAuthSeparate") }}</Button>
|
||||
<div class="grid max-w-md gap-1.5">
|
||||
<div class="grid gap-1.5">
|
||||
<div class="flex h-9 items-center gap-1 rounded-md border bg-muted/20 p-0.5">
|
||||
<Button type="button" size="sm" class="h-8 flex-1" :variant="nacosAuthKind === 'none' ? 'default' : 'ghost'" @click="nacosAuthKind = 'none'">{{ t("connection.nacosAuthNone") }}</Button>
|
||||
<Button type="button" size="sm" class="h-8 flex-1" :variant="nacosAuthKind === 'usernamePassword' ? 'default' : 'ghost'" @click="nacosAuthKind = 'usernamePassword'">{{ t("nacos.nacosUsernamePassword") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nacosConsoleAuthKind === 'inherit' && nacosAuthKind === 'none'" class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs text-destructive">{{ t("connection.nacosConsoleAuthPrimaryNone") }}</p>
|
||||
</div>
|
||||
<div v-if="nacosAuthKind === 'usernamePassword'" class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.user") }}</Label>
|
||||
<Input v-model="nacosUsername" placeholder="nacos" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="nacosPassword" />
|
||||
</div>
|
||||
<template v-if="nacosConsoleAuthKind === 'usernamePassword'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleUser") }}</Label
|
||||
><Input v-model="nacosConsoleUsername" class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsolePassword") }}</Label
|
||||
><PasswordInput v-model="nacosConsolePassword" class="col-span-3" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosAuth") }}</Label>
|
||||
<div class="col-span-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" :variant="nacosAuthKind === 'none' ? 'default' : 'outline'" @click="nacosAuthKind = 'none'">{{ t("connection.nacosAuthNone") }}</Button>
|
||||
<Button size="sm" :variant="nacosAuthKind === 'usernamePassword' ? 'default' : 'outline'" @click="nacosAuthKind = 'usernamePassword'">{{ t("connection.nacosAuthUserPassword") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="nacosAuthKind === 'usernamePassword'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.user") }}</Label>
|
||||
<Input v-model="nacosUsername" class="col-span-3" placeholder="nacos" />
|
||||
</section>
|
||||
|
||||
<section data-nacos-advanced-hint class="flex items-start gap-3 rounded-lg border border-dashed bg-muted/20 px-4 py-3">
|
||||
<CircleHelp class="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium">{{ t("nacos.nacosAdvancedHint") }}</div>
|
||||
<p class="mt-0.5 text-xs leading-5 text-muted-foreground">{{ t("nacos.nacosAdvancedHintDescription") }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="nacosPassword" class="col-span-3" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.nacosTls") }}</Label>
|
||||
<label class="col-span-3 inline-flex items-center gap-2">
|
||||
<input type="checkbox" v-model="nacosTlsSkipVerify" class="mr-0" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.nacosTlsSkipVerify") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosPageSize") }}</Label>
|
||||
<Input v-model.number="nacosPageSize" type="number" min="1" max="500" class="col-span-3" />
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" class="shrink-0" @click="configTab = 'advanced'">{{ t("nacos.nacosGoAdvanced") }}</Button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- Redis: host, port, user, password, ssl -->
|
||||
|
|
@ -6674,7 +6623,7 @@ function openExternalUrl(url: string) {
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="supportsTlsToggle" value="tls" class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2 pb-6">
|
||||
<div v-if="!supportsPostgresTlsOptions && !supportsMysqlTlsOptions" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">SSL/TLS</Label>
|
||||
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
|
||||
|
|
@ -6974,7 +6923,101 @@ function openExternalUrl(url: string) {
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent value="advanced" class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto pt-4 pr-2">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto pt-4 pr-2 pb-6">
|
||||
<section v-if="form.db_type === 'nacos'" data-nacos-advanced-settings class="overflow-hidden rounded-lg border">
|
||||
<div class="border-b bg-muted/20 px-4 py-3">
|
||||
<div class="text-sm font-medium">{{ t("nacos.nacosAdvancedTitle") }}</div>
|
||||
<p class="mt-0.5 text-xs leading-5 text-muted-foreground">{{ t("nacos.nacosAdvancedDescription") }}</p>
|
||||
</div>
|
||||
<div class="grid gap-5 p-4">
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.nacosPageSize") }}</Label>
|
||||
<Input v-model.number="nacosPageSize" type="number" min="1" max="500" />
|
||||
<p class="text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosPageSizeHint") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 border-t pt-4">
|
||||
<div class="grid gap-1.5 sm:grid-cols-[minmax(0,1fr)_180px] sm:items-center">
|
||||
<div>
|
||||
<Label>{{ t("connection.nacosMetrics") }}</Label>
|
||||
<p class="mt-1 text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosMetricsHint") }}</p>
|
||||
</div>
|
||||
<Select v-model="nacosMetricsMode">
|
||||
<SelectTrigger class="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 === 'custom'" class="grid gap-1.5">
|
||||
<Label>{{ t("connection.nacosMetricsUrl") }}</Label>
|
||||
<Input v-model="nacosMetricsUrl" :aria-invalid="!!nacosMetricsUrlError" :class="{ 'border-destructive focus-visible:ring-destructive': nacosMetricsUrlError }" placeholder="http://127.0.0.1:8848/nacos/actuator/prometheus" />
|
||||
<p v-if="nacosMetricsUrlError" class="text-xs text-destructive">{{ nacosMetricsUrlError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="nacosImplementation === 'rnacos'" class="grid gap-4 border-t pt-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<Label>{{ t("nacos.nacosRnacosExtension") }}</Label>
|
||||
<p class="mt-1 text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosRnacosExtensionHint") }}</p>
|
||||
</div>
|
||||
<label class="inline-flex shrink-0 items-center gap-2">
|
||||
<Switch v-model="nacosHistoryEnabled" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("nacos.nacosEnabled") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<template v-if="nacosHistoryEnabled">
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.nacosRNacosConsoleUrl") }}</Label>
|
||||
<Input v-model="nacosRNacosConsoleAddr" :placeholder="t('connection.nacosRNacosConsoleUrlPlaceholder')" />
|
||||
<p class="text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosRnacosConsoleUrlHint") }}</p>
|
||||
</div>
|
||||
<template v-if="nacosRNacosConsoleAddr.trim()">
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.nacosConsoleAuthentication") }}</Label>
|
||||
<div class="flex items-center gap-1 rounded-md border bg-muted/20 p-0.5">
|
||||
<Button type="button" size="sm" class="h-8 flex-1" :variant="nacosConsoleAuthKind === 'inherit' ? 'default' : 'ghost'" :disabled="nacosAuthKind === 'none'" @click="nacosConsoleAuthKind = 'inherit'">
|
||||
{{ t("connection.nacosConsoleAuthInherit") }}
|
||||
</Button>
|
||||
<Button type="button" size="sm" class="h-8 flex-1" :variant="nacosConsoleAuthKind === 'usernamePassword' ? 'default' : 'ghost'" @click="nacosConsoleAuthKind = 'usernamePassword'">
|
||||
{{ t("connection.nacosConsoleAuthSeparate") }}
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="nacosConsoleAuthKind === 'inherit' && nacosAuthKind === 'none'" class="text-xs text-destructive">{{ t("connection.nacosConsoleAuthPrimaryNone") }}</p>
|
||||
</div>
|
||||
<div v-if="nacosConsoleAuthKind === 'usernamePassword'" class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.nacosConsoleUser") }}</Label>
|
||||
<Input v-model="nacosConsoleUsername" />
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.nacosConsolePassword") }}</Label>
|
||||
<PasswordInput v-model="nacosConsolePassword" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<p v-else class="text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosRnacosDisabledHint") }}</p>
|
||||
</div>
|
||||
|
||||
<label class="flex items-start justify-between gap-4 border-t pt-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium">{{ t("connection.nacosTls") }}</div>
|
||||
<p class="mt-1 text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosTlsHint") }}</p>
|
||||
</div>
|
||||
<span class="inline-flex shrink-0 items-center gap-2">
|
||||
<input v-model="nacosTlsSkipVerify" type="checkbox" class="mr-0" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.nacosTlsSkipVerify") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="showGaussdbConnectionMode" class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.gaussdbConnectionMode") }}</Label>
|
||||
<div class="col-span-3 grid gap-1">
|
||||
|
|
@ -7131,7 +7174,7 @@ function openExternalUrl(url: string) {
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="canUseTransportLayers" value="transport" class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2">
|
||||
<div class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2 pb-6">
|
||||
<div class="connection-label-wide-grid grid min-w-0 grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.sshHops") }}</Label>
|
||||
<div class="col-span-3 grid min-w-0 gap-3">
|
||||
|
|
@ -7397,7 +7440,12 @@ function openExternalUrl(url: string) {
|
|||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
<Button v-if="canChooseVisibleDatabases" variant="outline" class="shrink-0" :disabled="isTesting || isSaving || isLoadingVisibleDatabases || !hasRequiredConnectionTarget" @click="openVisibleDatabasesPicker">
|
||||
<Button v-if="canChooseVisibleNacosNamespaces" variant="outline" class="shrink-0" :disabled="isTesting || isSaving || isLoadingVisibleNacosNamespaces || !hasRequiredConnectionTarget" @click="openVisibleNacosNamespacesPicker">
|
||||
<Loader2 v-if="isLoadingVisibleNacosNamespaces" class="mr-1.5 h-4 w-4 animate-spin" />
|
||||
<ListFilter v-else class="mr-1.5 h-4 w-4" />
|
||||
{{ t("nacos.nacosVisibleNamespacesTitle") }}
|
||||
</Button>
|
||||
<Button v-else-if="canChooseVisibleDatabases" variant="outline" class="shrink-0" :disabled="isTesting || isSaving || isLoadingVisibleDatabases || !hasRequiredConnectionTarget" @click="openVisibleDatabasesPicker">
|
||||
<Loader2 v-if="isLoadingVisibleDatabases" class="mr-1.5 h-4 w-4 animate-spin" />
|
||||
<ListFilter v-else class="mr-1.5 h-4 w-4" />
|
||||
{{ hasVisibleObjectFilter ? visibleObjectSummary : visibleFilterUsesSchemas ? t("contextMenu.configureVisibleObjects") : t("contextMenu.selectVisibleDatabases") }}
|
||||
|
|
@ -7481,6 +7529,58 @@ function openExternalUrl(url: string) {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showVisibleNacosNamespacesDialog">
|
||||
<DialogContent class="sm:max-w-[460px]" @interact-outside.prevent @escape-key-down.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.nacosVisibleNamespacesTitle") }}</DialogTitle>
|
||||
<p class="text-sm text-muted-foreground">{{ t("nacos.nacosVisibleNamespacesDescription", { name: form.name || selectedProfile().label }) }}</p>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="flex items-center gap-2 rounded-md border bg-background px-2">
|
||||
<Search class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<Input v-model="visibleNacosNamespaceSearchText" :placeholder="t('nacos.nacosSearchNamespaces')" class="h-8 border-0 px-0 shadow-none focus-visible:ring-0" :disabled="isLoadingVisibleNacosNamespaces || !!visibleNacosNamespaceError" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ t("nacos.nacosSelectedNamespaces", { selected: visibleNacosNamespaceSelectedCount, total: visibleNacosNamespaces.length }) }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="selectAllVisibleNacosNamespaces">{{ t("nacos.nacosSelectAll") }}</button>
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="clearVisibleNacosNamespaceSelection">{{ t("nacos.nacosClearSelection") }}</button>
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="showAllVisibleNacosNamespaces">{{ t("nacos.nacosShowAll") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!isLoadingVisibleNacosNamespaces && !visibleNacosNamespaceError && !visibleNacosNamespaceCanSave" class="text-xs text-destructive">{{ t("nacos.nacosNamespaceSelectionRequired") }}</p>
|
||||
|
||||
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
||||
<div v-if="isLoadingVisibleNacosNamespaces" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
<div v-else-if="visibleNacosNamespaceError" class="p-3 text-sm text-destructive">{{ t("nacos.nacosLoadNamespacesFailed", { message: visibleNacosNamespaceError }) }}</div>
|
||||
<div v-else-if="!filteredVisibleNacosNamespaces.length" class="p-3 text-sm text-muted-foreground">{{ t("grid.noSearchResults") }}</div>
|
||||
<template v-else>
|
||||
<button
|
||||
v-for="namespace in filteredVisibleNacosNamespaces"
|
||||
:key="nacosNamespaceValue(namespace) || '__public__'"
|
||||
type="button"
|
||||
class="flex min-h-9 w-full min-w-0 items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none"
|
||||
@click="toggleVisibleNacosNamespace(nacosNamespaceValue(namespace))"
|
||||
>
|
||||
<CheckSquare v-if="visibleNacosNamespaceSelection.has(nacosNamespaceValue(namespace))" class="h-4 w-4 shrink-0 text-primary" />
|
||||
<Square v-else class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ nacosNamespaceLabel(namespace) }}</span>
|
||||
<span v-if="namespace.namespace && namespace.namespace !== nacosNamespaceLabel(namespace)" class="shrink-0 truncate text-xs text-muted-foreground">{{ namespace.namespace }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showVisibleNacosNamespacesDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="isLoadingVisibleNacosNamespaces || !!visibleNacosNamespaceError || !visibleNacosNamespaceCanSave" @click="saveVisibleNacosNamespaceSelection">{{ t("nacos.save") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showVisibleDatabasesDialog">
|
||||
<DialogContent class="sm:max-w-[520px]" @keydown="preventDialogDocumentSelectAll">
|
||||
<DialogHeader>
|
||||
|
|
@ -7654,16 +7754,23 @@ function openExternalUrl(url: string) {
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.connection-dialog-content--config .connection-form-body {
|
||||
/* Preserve every form section's natural height; the form viewport owns
|
||||
* scrolling and must never shrink cards into collapsed grid rows. */
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.connection-form-body--nacos {
|
||||
/* Authentication fields are conditional. Keep every Nacos card at its
|
||||
* max-content height when they appear, and scroll the form as a whole. */
|
||||
grid-auto-rows: max-content;
|
||||
}
|
||||
|
||||
@media (max-height: 720px) {
|
||||
.connection-dialog-content--config {
|
||||
/* A definite flex height lets tab bodies shrink and scroll above the fixed footer. */
|
||||
height: calc(var(--dbx-viewport-height) - 2rem);
|
||||
}
|
||||
|
||||
.connection-dialog-content--config .connection-form-body {
|
||||
/* Keep grid rows compact when the scroll viewport is taller than the form. */
|
||||
align-content: start;
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy responsive layout rules live in public/connection-dialog-legacy.css
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,6 +6,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import type { NacosBatchPreview, NacosBatchReport, NacosConfigSelectionScope, NacosConflictPolicy, NacosNamespaceInfo } from "@/types/nacos";
|
||||
import { nacosNamespaceIdentity } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
|
||||
export type NacosBatchDialogMode = "export" | "import" | "copy";
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ const targetNamespace = ref("");
|
|||
|
||||
const titleKey = computed(() => `nacos.batch${props.mode[0].toUpperCase()}${props.mode.slice(1)}Title`);
|
||||
const descriptionKey = computed(() => `nacos.batch${props.mode[0].toUpperCase()}${props.mode.slice(1)}Description`);
|
||||
const targetNamespaces = computed(() => props.namespaces.filter((item) => props.targetConnectionId !== props.sourceConnectionId || item.namespace !== props.currentNamespace));
|
||||
const targetNamespaces = computed(() => props.namespaces.filter((item) => props.targetConnectionId !== props.sourceConnectionId || nacosNamespaceIdentity(item.namespace) !== nacosNamespaceIdentity(props.currentNamespace)));
|
||||
const selectedTargetNamespace = computed(() => {
|
||||
try {
|
||||
const namespace = JSON.parse(targetNamespace.value);
|
||||
|
|
|
|||
|
|
@ -39,4 +39,55 @@ describe("NacosAdminConsole config workbench layout", () => {
|
|||
expect(source.indexOf("batchTransferRequest.value = null;", staleBranch)).toBeGreaterThan(staleBranch);
|
||||
expect(source.indexOf('batchError.value = t("nacos.previewExpired");', staleBranch)).toBeGreaterThan(staleBranch);
|
||||
});
|
||||
|
||||
it("keeps service detail and instance loading as independent guarded requests", () => {
|
||||
expect(source).toContain("const serviceDetailRequestGuard = createNacosLatestRequestGuard();");
|
||||
expect(source).toContain("const instancesRequestGuard = createNacosLatestRequestGuard();");
|
||||
expect(source).toContain("await Promise.all([loadServiceDetail(), loadInstances()]);");
|
||||
});
|
||||
|
||||
it("keeps instance weight edits as drafts until the explicit save action", () => {
|
||||
expect(source).toContain("instanceWeightDrafts.value[instanceIdentity(instance)] = String(value);");
|
||||
expect(source).toContain('@click="requestInstanceWeightUpdate(instance)"');
|
||||
expect(source).not.toContain('@change="requestInstanceWeightUpdate(instance)"');
|
||||
});
|
||||
|
||||
it("tracks instance operations by token so stale requests cannot lock a row forever", () => {
|
||||
expect(source).toContain("const updatingInstanceKeys = ref<Record<string, number>>({});");
|
||||
expect(source).toContain("const operationToken = beginInstanceOperation(key);");
|
||||
expect(source).toContain("clearInstanceOperation(key, operationToken);");
|
||||
expect(source).not.toContain('if (updateId === instanceUpdateSequence) updatingInstanceKey.value = "";');
|
||||
});
|
||||
|
||||
it("prevents service-management dialogs from closing through outside clicks or Escape", () => {
|
||||
expect(source.match(/@pointer-down-outside\.prevent/g)?.length).toBeGreaterThanOrEqual(3);
|
||||
expect(source.match(/@interact-outside\.prevent/g)?.length).toBeGreaterThanOrEqual(3);
|
||||
expect(source.match(/@escape-key-down\.prevent/g)?.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it("renders service details before the independently scrollable instance cards", () => {
|
||||
const detail = source.indexOf("nacos.serviceDetails");
|
||||
const instanceCards = source.indexOf('v-for="instance in instances"');
|
||||
expect(detail).toBeGreaterThan(0);
|
||||
expect(instanceCards).toBeGreaterThan(detail);
|
||||
});
|
||||
|
||||
it("keeps verbose service details collapsed until the user expands them", () => {
|
||||
expect(source).toContain("const serviceDetailExpanded = ref(false);");
|
||||
});
|
||||
|
||||
it("reserves the cluster-clear action space so entering a filter cannot reflow the toolbar", () => {
|
||||
expect(source).toContain('class="min-w-0 flex-1"');
|
||||
expect(source).toContain('class="flex shrink-0 items-center gap-1"');
|
||||
expect(source).toContain(':class="{ invisible: !serviceCluster }"');
|
||||
expect(source).toContain(':disabled="instancesLoading || !serviceCluster"');
|
||||
expect(source).not.toContain('v-if="serviceCluster"\n size="sm"');
|
||||
});
|
||||
|
||||
it("separates the service header, filtering controls, and management actions", () => {
|
||||
expect(source).toContain('<header class="shrink-0 border-b bg-background">');
|
||||
expect(source).toContain('class="flex flex-wrap items-center gap-x-4 gap-y-2 border-t bg-muted/30 px-4 py-2"');
|
||||
expect(source).toContain('t("nacos.serviceSettings")');
|
||||
expect(source).toContain('t("nacos.registerInstance")');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7007,5 +7007,98 @@ export default {
|
|||
next: "Next",
|
||||
test: "Test",
|
||||
readOnly: "Read only",
|
||||
nacosConnectionPlan: "Connection plan",
|
||||
nacosConnectionPlanDescription: "Choose the service implementation and API version.",
|
||||
nacosServiceAddress: "Service address",
|
||||
nacosServiceAddressHint: "Enter the service address. The default port is 8848, for example http://host:8848/nacos.",
|
||||
nacosAuth: "Authentication",
|
||||
nacosAuthHint: "These credentials are used for connection tests and daily management.",
|
||||
nacosUsernamePassword: "Username / password",
|
||||
nacosAdvancedHint: "More connection options are available on the Advanced tab.",
|
||||
nacosAdvancedHintDescription: "Configure Prometheus metrics, page size, TLS verification, and r-nacos extensions there.",
|
||||
nacosGoAdvanced: "Go to Advanced",
|
||||
nacosV3ConsolePortWarning: "8080 is the default Nacos 3 Web Console port. Use the Server / Admin API address on port 8848 for service management, for example http://127.0.0.1:8848/nacos.",
|
||||
nacosAdvancedTitle: "Nacos advanced connection options",
|
||||
nacosAdvancedDescription: "These settings have safe defaults and usually only need adjustment for custom deployments, metrics, or r-nacos extensions.",
|
||||
nacosPageSizeHint: "Used for configuration and service lists; 1–500 items per page.",
|
||||
nacosMetricsHint: "Query metrics from the service address automatically; choose a custom URL when the metrics endpoint differs.",
|
||||
nacosRnacosExtension: "r-nacos extensions",
|
||||
nacosRnacosExtensionHint: "Enables config history, config descriptions, and disabled-instance management.",
|
||||
nacosEnabled: "Enabled",
|
||||
nacosRnacosConsoleUrlHint: "Usually http://host:10848.",
|
||||
nacosRnacosDisabledHint: "When disabled, only the service address is used; basic configuration and service management remain available.",
|
||||
nacosTlsHint: "Enable only for a trusted self-signed HTTPS certificate.",
|
||||
nacosVisibleNamespacesTitle: "Choose visible namespaces",
|
||||
nacosVisibleNamespacesDescription: "Choose the namespaces to show in the sidebar for {name}.",
|
||||
nacosSearchNamespaces: "Search namespaces...",
|
||||
nacosSelectedNamespaces: "{selected}/{total} selected",
|
||||
nacosSelectAll: "Select all",
|
||||
nacosClearSelection: "Clear",
|
||||
nacosShowAll: "Show all",
|
||||
nacosNamespaceSelectionRequired: "Select at least one namespace, or use “Show all” to clear the filter.",
|
||||
nacosLoadNamespacesFailed: "Failed to load namespaces: {message}",
|
||||
capabilityReadOnly: "This connection is read-only.",
|
||||
capabilityReadOnlyWrite: "This implementation currently exposes read operations only; write compatibility is not verified.",
|
||||
capabilityVersionUnsupported: "This Nacos version does not support the operation.",
|
||||
capabilityEndpointUnavailable: "This connection does not provide the management endpoint.",
|
||||
capabilityNotVerified: "This operation has not been verified for the current implementation.",
|
||||
capabilityHeaderReadOnly: "This connection is read-only; service and instance writes are disabled.",
|
||||
protectionTriggeredDescription: "Protection mode is triggered because the healthy-instance ratio is below the configured threshold. Nacos may still return unhealthy instances instead of an empty list.",
|
||||
instanceEphemeral: "Ephemeral instance",
|
||||
instancePersistent: "Persistent instance",
|
||||
instanceUnknown: "Unknown instance type",
|
||||
instanceConfirmDetails: "Namespace: {namespace}\nService: {service}\nAddress: {ip}:{port}\nCluster: {cluster}\nType: {type}",
|
||||
serviceUpdateUnconfirmed: "The service update was submitted, but Nacos has not returned the target state. Refresh the service details to confirm.",
|
||||
invalidWeight: "Weight must be a finite number greater than or equal to 0.",
|
||||
invalidWeightInput: "Enter a finite weight greater than or equal to 0.",
|
||||
metadataLabel: "Metadata",
|
||||
selectorLabel: "Selector",
|
||||
instanceUpdateUnconfirmed: "The instance update was submitted, but Nacos has not returned the new state. Refresh to check the server state.",
|
||||
instanceRegisterUnconfirmed: "Instance registration was submitted, but Nacos has not returned the instance. Refresh to confirm.",
|
||||
instanceDeregisterUnconfirmed: "Instance deregistration was submitted, but Nacos still returns the instance. Refresh to confirm.",
|
||||
serviceValidation: "Service name and group are required; the protection threshold must be between 0 and 1.",
|
||||
serviceHasInstances: "The service still contains instances and cannot be deleted.",
|
||||
serviceDeleteUnconfirmed: "Service deletion was submitted, but Nacos still returns the service. Reload and confirm.",
|
||||
instanceValidation: "Enter a valid IP, port, and non-negative weight.",
|
||||
protectionTriggered: "Protection mode triggered",
|
||||
loadedInstances: "{count} loaded instances",
|
||||
filterInstanceCluster: "Filter instance cluster",
|
||||
filter: "Filter",
|
||||
clear: "Clear",
|
||||
serviceSettings: "Service settings",
|
||||
serviceDetails: "Service details",
|
||||
expand: "Expand",
|
||||
instanceStat: "Instances",
|
||||
healthyInstances: "Healthy instances",
|
||||
clusterCount: "Clusters",
|
||||
protectThreshold: "Protection threshold",
|
||||
itemCount: "{count} items",
|
||||
temporaryService: "Ephemeral service",
|
||||
loadingServiceDetail: "Loading service details…",
|
||||
refreshDetail: "Refresh details",
|
||||
noMetadata: "No metadata",
|
||||
noInstances: "No instances",
|
||||
createNacosService: "Create Nacos service",
|
||||
editNacosService: "Edit Nacos service",
|
||||
manageServiceMetadata: "Manage service metadata and protection threshold.",
|
||||
serviceName: "Service name",
|
||||
groupName: "Group",
|
||||
thresholdExample: "For example 0.5",
|
||||
jsonObject: "JSON object",
|
||||
optionalJsonObject: "JSON object, optional",
|
||||
jsonObjectValidation: " must be a JSON object.",
|
||||
registerPersistentInstance: "Register persistent instance",
|
||||
persistentInstanceHint: "DBX does not register ephemeral instances because it cannot maintain their heartbeat.",
|
||||
ipAddress: "IP address",
|
||||
port: "Port",
|
||||
instanceEditorTitle: "Edit instance",
|
||||
deleteNacosService: "Delete Nacos service",
|
||||
confirmEmptyServiceDelete: "Delete this empty service? This action cannot be undone.",
|
||||
deregisterNacosInstance: "Deregister Nacos instance",
|
||||
confirmInstanceDeregister: "Deregister this instance from the current service?",
|
||||
edit: "Edit",
|
||||
restore: "Restore",
|
||||
registerInstance: "Register instance",
|
||||
deregister: "Deregister",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5730,6 +5730,99 @@ export default withEnglishFallback({
|
|||
next: "Siguiente",
|
||||
test: "Probar",
|
||||
readOnly: "Solo lectura",
|
||||
nacosConnectionPlan: "Plan de conexión",
|
||||
nacosConnectionPlanDescription: "Elige la implementación del servicio y la versión de la API.",
|
||||
nacosServiceAddress: "Dirección del servicio",
|
||||
nacosServiceAddressHint: "Introduce la dirección del servicio. El puerto predeterminado es 8848, por ejemplo http://host:8848/nacos.",
|
||||
nacosAuth: "Autenticación",
|
||||
nacosAuthHint: "Estas credenciales se usan para probar la conexión y para la administración diaria.",
|
||||
nacosUsernamePassword: "Usuario / contraseña",
|
||||
nacosAdvancedHint: "Hay más opciones de conexión en la pestaña Avanzado.",
|
||||
nacosAdvancedHintDescription: "Configura allí las métricas de Prometheus, el tamaño de página, la validación TLS y las extensiones de r-nacos.",
|
||||
nacosGoAdvanced: "Ir a Avanzado",
|
||||
nacosV3ConsolePortWarning: "8080 es el puerto predeterminado de la consola web de Nacos 3. Para administrar servicios, usa la dirección Server / Admin API del puerto 8848, por ejemplo http://127.0.0.1:8848/nacos.",
|
||||
nacosAdvancedTitle: "Opciones avanzadas de conexión de Nacos",
|
||||
nacosAdvancedDescription: "Estos parámetros tienen valores seguros y normalmente solo deben ajustarse para despliegues personalizados, métricas o extensiones de r-nacos.",
|
||||
nacosPageSizeHint: "Se usa para las listas de configuraciones y servicios; entre 1 y 500 elementos por página.",
|
||||
nacosMetricsHint: "Consulta automáticamente las métricas desde la dirección del servicio; elige una URL personalizada si el endpoint es diferente.",
|
||||
nacosRnacosExtension: "Extensiones de r-nacos",
|
||||
nacosRnacosExtensionHint: "Permite consultar el historial y las descripciones de configuración, y administrar instancias deshabilitadas.",
|
||||
nacosEnabled: "Habilitado",
|
||||
nacosRnacosConsoleUrlHint: "Normalmente http://host:10848.",
|
||||
nacosRnacosDisabledHint: "Si está deshabilitado, solo se usa la dirección del servicio; la configuración y la administración básica de servicios siguen disponibles.",
|
||||
nacosTlsHint: "Actívalo solo para un certificado HTTPS autofirmado de confianza.",
|
||||
nacosVisibleNamespacesTitle: "Elegir namespaces visibles",
|
||||
nacosVisibleNamespacesDescription: "Elige los namespaces que se mostrarán en la barra lateral de {name}.",
|
||||
nacosSearchNamespaces: "Buscar namespaces...",
|
||||
nacosSelectedNamespaces: "{selected}/{total} seleccionados",
|
||||
nacosSelectAll: "Seleccionar todo",
|
||||
nacosClearSelection: "Borrar",
|
||||
nacosShowAll: "Mostrar todo",
|
||||
nacosNamespaceSelectionRequired: "Selecciona al menos un namespace o usa “Mostrar todo” para borrar el filtro.",
|
||||
nacosLoadNamespacesFailed: "No se pudieron cargar los namespaces: {message}",
|
||||
capabilityReadOnly: "Esta conexión es de solo lectura.",
|
||||
capabilityReadOnlyWrite: "Esta implementación solo expone operaciones de lectura; la compatibilidad de escritura no está verificada.",
|
||||
capabilityVersionUnsupported: "Esta versión de Nacos no admite la operación.",
|
||||
capabilityEndpointUnavailable: "Esta conexión no proporciona el endpoint de administración.",
|
||||
capabilityNotVerified: "Esta operación no se ha verificado para la implementación actual.",
|
||||
capabilityHeaderReadOnly: "Esta conexión es de solo lectura; las escrituras de servicios e instancias están deshabilitadas.",
|
||||
protectionTriggeredDescription: "El modo de protección está activo porque la proporción de instancias saludables es inferior al umbral configurado. Nacos aún puede devolver instancias no saludables en lugar de una lista vacía.",
|
||||
instanceEphemeral: "Instancia efímera",
|
||||
instancePersistent: "Instancia persistente",
|
||||
instanceUnknown: "Tipo de instancia desconocido",
|
||||
instanceConfirmDetails: "Namespace: {namespace}\nServicio: {service}\nDirección: {ip}:{port}\nClúster: {cluster}\nTipo: {type}",
|
||||
serviceUpdateUnconfirmed: "La actualización del servicio se envió, pero Nacos aún no ha devuelto el estado solicitado. Actualiza los detalles del servicio para confirmarlo.",
|
||||
invalidWeight: "El peso debe ser un número finito mayor o igual que 0.",
|
||||
invalidWeightInput: "Introduce un peso finito mayor o igual que 0.",
|
||||
metadataLabel: "Metadatos",
|
||||
selectorLabel: "Selector",
|
||||
instanceUpdateUnconfirmed: "La actualización de la instancia se envió, pero Nacos aún no ha devuelto el nuevo estado. Actualiza para comprobar el estado del servidor.",
|
||||
instanceRegisterUnconfirmed: "El registro de la instancia se envió, pero Nacos aún no la ha devuelto. Actualiza para confirmarlo.",
|
||||
instanceDeregisterUnconfirmed: "La baja de la instancia se envió, pero Nacos todavía la devuelve. Actualiza para confirmarlo.",
|
||||
serviceValidation: "El nombre del servicio y el grupo son obligatorios; el umbral de protección debe estar entre 0 y 1.",
|
||||
serviceHasInstances: "El servicio todavía contiene instancias y no se puede eliminar.",
|
||||
serviceDeleteUnconfirmed: "La eliminación del servicio se envió, pero Nacos todavía lo devuelve. Recarga y confirma.",
|
||||
instanceValidation: "Introduce una IP, un puerto y un peso no negativo válidos.",
|
||||
protectionTriggered: "Modo de protección activo",
|
||||
loadedInstances: "{count} instancias cargadas",
|
||||
filterInstanceCluster: "Filtrar clúster de instancia",
|
||||
filter: "Filtrar",
|
||||
clear: "Borrar",
|
||||
serviceSettings: "Ajustes del servicio",
|
||||
serviceDetails: "Detalles del servicio",
|
||||
expand: "Expandir",
|
||||
instanceStat: "Instancias",
|
||||
healthyInstances: "Instancias saludables",
|
||||
clusterCount: "Clústeres",
|
||||
protectThreshold: "Umbral de protección",
|
||||
itemCount: "{count} elementos",
|
||||
temporaryService: "Servicio efímero",
|
||||
loadingServiceDetail: "Cargando detalles del servicio…",
|
||||
refreshDetail: "Actualizar detalles",
|
||||
noMetadata: "Sin metadatos",
|
||||
noInstances: "No hay instancias",
|
||||
createNacosService: "Crear servicio Nacos",
|
||||
editNacosService: "Editar servicio Nacos",
|
||||
manageServiceMetadata: "Administra los metadatos y el umbral de protección del servicio.",
|
||||
serviceName: "Nombre del servicio",
|
||||
groupName: "Grupo",
|
||||
thresholdExample: "Por ejemplo, 0.5",
|
||||
jsonObject: "Objeto JSON",
|
||||
optionalJsonObject: "Objeto JSON, opcional",
|
||||
jsonObjectValidation: " debe ser un objeto JSON.",
|
||||
registerPersistentInstance: "Registrar instancia persistente",
|
||||
persistentInstanceHint: "DBX no registra instancias efímeras porque no puede mantener sus latidos.",
|
||||
ipAddress: "Dirección IP",
|
||||
port: "Puerto",
|
||||
instanceEditorTitle: "Editar instancia",
|
||||
deleteNacosService: "Eliminar servicio Nacos",
|
||||
confirmEmptyServiceDelete: "¿Eliminar este servicio vacío? Esta acción no se puede deshacer.",
|
||||
deregisterNacosInstance: "Dar de baja instancia Nacos",
|
||||
confirmInstanceDeregister: "¿Dar de baja esta instancia del servicio actual?",
|
||||
edit: "Editar",
|
||||
restore: "Restaurar",
|
||||
registerInstance: "Registrar instancia",
|
||||
deregister: "Dar de baja",
|
||||
},
|
||||
gridfsBrowser: {
|
||||
bucketCount: "Cantidad de buckets",
|
||||
|
|
|
|||
|
|
@ -5730,6 +5730,99 @@ export default withEnglishFallback({
|
|||
next: "Succ.",
|
||||
test: "Test",
|
||||
readOnly: "Sola lettura",
|
||||
nacosConnectionPlan: "Piano di connessione",
|
||||
nacosConnectionPlanDescription: "Scegli l'implementazione del servizio e la versione API.",
|
||||
nacosServiceAddress: "Indirizzo del servizio",
|
||||
nacosServiceAddressHint: "Inserisci l'indirizzo del servizio. La porta predefinita è 8848, ad esempio http://host:8848/nacos.",
|
||||
nacosAuth: "Autenticazione",
|
||||
nacosAuthHint: "Queste credenziali vengono usate per i test di connessione e la gestione quotidiana.",
|
||||
nacosUsernamePassword: "Nome utente / password",
|
||||
nacosAdvancedHint: "Altre opzioni di connessione sono disponibili nella scheda Avanzate.",
|
||||
nacosAdvancedHintDescription: "Configura qui metriche Prometheus, dimensione pagina, verifica TLS ed estensioni r-nacos.",
|
||||
nacosGoAdvanced: "Vai ad Avanzate",
|
||||
nacosV3ConsolePortWarning: "8080 è la porta predefinita della Web Console Nacos 3. Per la gestione dei servizi usa l'indirizzo Server / Admin API sulla porta 8848, ad esempio http://127.0.0.1:8848/nacos.",
|
||||
nacosAdvancedTitle: "Opzioni di connessione avanzate Nacos",
|
||||
nacosAdvancedDescription: "Questi parametri hanno valori sicuri e di solito richiedono modifiche solo per distribuzioni personalizzate, metriche o estensioni r-nacos.",
|
||||
nacosPageSizeHint: "Usato per gli elenchi di configurazioni e servizi; da 1 a 500 elementi per pagina.",
|
||||
nacosMetricsHint: "Interroga automaticamente le metriche dall'indirizzo del servizio; scegli un URL personalizzato se l'endpoint è diverso.",
|
||||
nacosRnacosExtension: "Estensioni r-nacos",
|
||||
nacosRnacosExtensionHint: "Abilita la cronologia e le descrizioni delle configurazioni e la gestione delle istanze disabilitate.",
|
||||
nacosEnabled: "Abilitato",
|
||||
nacosRnacosConsoleUrlHint: "Di solito http://host:10848.",
|
||||
nacosRnacosDisabledHint: "Quando è disabilitato viene usato solo l'indirizzo del servizio; la configurazione e la gestione di base dei servizi restano disponibili.",
|
||||
nacosTlsHint: "Abilita solo per un certificato HTTPS autofirmato attendibile.",
|
||||
nacosVisibleNamespacesTitle: "Scegli i namespace visibili",
|
||||
nacosVisibleNamespacesDescription: "Scegli i namespace da mostrare nella barra laterale di {name}.",
|
||||
nacosSearchNamespaces: "Cerca namespace...",
|
||||
nacosSelectedNamespaces: "{selected}/{total} selezionati",
|
||||
nacosSelectAll: "Seleziona tutto",
|
||||
nacosClearSelection: "Cancella",
|
||||
nacosShowAll: "Mostra tutto",
|
||||
nacosNamespaceSelectionRequired: "Seleziona almeno un namespace oppure usa “Mostra tutto” per cancellare il filtro.",
|
||||
nacosLoadNamespacesFailed: "Impossibile caricare i namespace: {message}",
|
||||
capabilityReadOnly: "Questa connessione è di sola lettura.",
|
||||
capabilityReadOnlyWrite: "Questa implementazione espone solo operazioni di lettura; la compatibilità di scrittura non è verificata.",
|
||||
capabilityVersionUnsupported: "Questa versione di Nacos non supporta l'operazione.",
|
||||
capabilityEndpointUnavailable: "Questa connessione non fornisce l'endpoint di gestione.",
|
||||
capabilityNotVerified: "Questa operazione non è stata verificata per l'implementazione corrente.",
|
||||
capabilityHeaderReadOnly: "Questa connessione è di sola lettura; le scritture di servizi e istanze sono disabilitate.",
|
||||
protectionTriggeredDescription: "La modalità di protezione è attiva perché la percentuale di istanze sane è inferiore alla soglia configurata. Nacos può comunque restituire istanze non sane invece di un elenco vuoto.",
|
||||
instanceEphemeral: "Istanza effimera",
|
||||
instancePersistent: "Istanza persistente",
|
||||
instanceUnknown: "Tipo di istanza sconosciuto",
|
||||
instanceConfirmDetails: "Namespace: {namespace}\nServizio: {service}\nIndirizzo: {ip}:{port}\nCluster: {cluster}\nTipo: {type}",
|
||||
serviceUpdateUnconfirmed: "L'aggiornamento del servizio è stato inviato, ma Nacos non ha ancora restituito lo stato previsto. Aggiorna i dettagli del servizio per confermare.",
|
||||
invalidWeight: "Il peso deve essere un numero finito maggiore o uguale a 0.",
|
||||
invalidWeightInput: "Inserisci un peso finito maggiore o uguale a 0.",
|
||||
metadataLabel: "Metadati",
|
||||
selectorLabel: "Selettore",
|
||||
instanceUpdateUnconfirmed: "L'aggiornamento dell'istanza è stato inviato, ma Nacos non ha ancora restituito il nuovo stato. Aggiorna per controllare lo stato del server.",
|
||||
instanceRegisterUnconfirmed: "La registrazione dell'istanza è stata inviata, ma Nacos non ha ancora restituito l'istanza. Aggiorna per confermare.",
|
||||
instanceDeregisterUnconfirmed: "La rimozione dell'istanza è stata inviata, ma Nacos restituisce ancora l'istanza. Aggiorna per confermare.",
|
||||
serviceValidation: "Nome del servizio e gruppo sono obbligatori; la soglia di protezione deve essere compresa tra 0 e 1.",
|
||||
serviceHasInstances: "Il servizio contiene ancora istanze e non può essere eliminato.",
|
||||
serviceDeleteUnconfirmed: "L'eliminazione del servizio è stata inviata, ma Nacos restituisce ancora il servizio. Ricarica e conferma.",
|
||||
instanceValidation: "Inserisci un IP, una porta e un peso non negativo validi.",
|
||||
protectionTriggered: "Modalità di protezione attiva",
|
||||
loadedInstances: "{count} istanze caricate",
|
||||
filterInstanceCluster: "Filtra cluster dell'istanza",
|
||||
filter: "Filtra",
|
||||
clear: "Cancella",
|
||||
serviceSettings: "Impostazioni servizio",
|
||||
serviceDetails: "Dettagli servizio",
|
||||
expand: "Espandi",
|
||||
instanceStat: "Istanze",
|
||||
healthyInstances: "Istanze sane",
|
||||
clusterCount: "Cluster",
|
||||
protectThreshold: "Soglia di protezione",
|
||||
itemCount: "{count} elementi",
|
||||
temporaryService: "Servizio effimero",
|
||||
loadingServiceDetail: "Caricamento dettagli servizio…",
|
||||
refreshDetail: "Aggiorna dettagli",
|
||||
noMetadata: "Nessun metadato",
|
||||
noInstances: "Nessuna istanza",
|
||||
createNacosService: "Crea servizio Nacos",
|
||||
editNacosService: "Modifica servizio Nacos",
|
||||
manageServiceMetadata: "Gestisci i metadati e la soglia di protezione del servizio.",
|
||||
serviceName: "Nome servizio",
|
||||
groupName: "Gruppo",
|
||||
thresholdExample: "Ad esempio 0,5",
|
||||
jsonObject: "Oggetto JSON",
|
||||
optionalJsonObject: "Oggetto JSON, facoltativo",
|
||||
jsonObjectValidation: " deve essere un oggetto JSON.",
|
||||
registerPersistentInstance: "Registra istanza persistente",
|
||||
persistentInstanceHint: "DBX non registra istanze effimere perché non può mantenerne l'heartbeat.",
|
||||
ipAddress: "Indirizzo IP",
|
||||
port: "Porta",
|
||||
instanceEditorTitle: "Modifica istanza",
|
||||
deleteNacosService: "Elimina servizio Nacos",
|
||||
confirmEmptyServiceDelete: "Eliminare questo servizio vuoto? L'azione non può essere annullata.",
|
||||
deregisterNacosInstance: "Rimuovi istanza Nacos",
|
||||
confirmInstanceDeregister: "Rimuovere questa istanza dal servizio corrente?",
|
||||
edit: "Modifica",
|
||||
restore: "Ripristina",
|
||||
registerInstance: "Registra istanza",
|
||||
deregister: "Rimuovi",
|
||||
},
|
||||
gridfsBrowser: {
|
||||
bucketCount: "Bucket",
|
||||
|
|
|
|||
|
|
@ -5785,6 +5785,99 @@ export default withEnglishFallback({
|
|||
namespaceNamePlaceholder: "名前空間名を入力",
|
||||
namespaceDesc: "説明",
|
||||
namespaceDescPlaceholder: "デフォルトは名前空間名",
|
||||
nacosConnectionPlan: "接続方式",
|
||||
nacosConnectionPlanDescription: "サービス実装と API バージョンを選択します。",
|
||||
nacosServiceAddress: "サービスアドレス",
|
||||
nacosServiceAddressHint: "サービスアドレスを入力してください。既定のポートは 8848 です(例: http://host:8848/nacos)。",
|
||||
nacosAuth: "認証",
|
||||
nacosAuthHint: "接続テストと日常の管理に使用する認証情報です。",
|
||||
nacosUsernamePassword: "ユーザー名 / パスワード",
|
||||
nacosAdvancedHint: "その他の接続オプションは「詳細」タブにあります。",
|
||||
nacosAdvancedHintDescription: "Prometheus メトリクス、ページサイズ、TLS 検証、r-nacos 拡張を設定できます。",
|
||||
nacosGoAdvanced: "詳細へ移動",
|
||||
nacosV3ConsolePortWarning: "8080 は Nacos 3 Web Console の既定ポートです。サービス管理には 8848 の Server / Admin API アドレス(例: http://127.0.0.1:8848/nacos)を使用してください。",
|
||||
nacosAdvancedTitle: "Nacos 詳細接続オプション",
|
||||
nacosAdvancedDescription: "安全な既定値が設定されています。通常はカスタム構成、メトリクス、r-nacos 拡張を使用する場合だけ変更します。",
|
||||
nacosPageSizeHint: "設定とサービスの一覧に使用します。1 ページあたり 1~500 件です。",
|
||||
nacosMetricsHint: "サービスアドレスからメトリクスを自動取得します。エンドポイントが異なる場合はカスタム URL を選択してください。",
|
||||
nacosRnacosExtension: "r-nacos 拡張",
|
||||
nacosRnacosExtensionHint: "設定履歴・設定説明の取得と、無効なインスタンスの管理を有効にします。",
|
||||
nacosEnabled: "有効",
|
||||
nacosRnacosConsoleUrlHint: "通常は http://host:10848 です。",
|
||||
nacosRnacosDisabledHint: "無効の場合はサービスアドレスだけを使用します。基本的な設定とサービス管理は引き続き利用できます。",
|
||||
nacosTlsHint: "信頼できる自己署名 HTTPS 証明書の場合だけ有効にしてください。",
|
||||
nacosVisibleNamespacesTitle: "表示する名前空間を選択",
|
||||
nacosVisibleNamespacesDescription: "「{name}」のサイドバーに表示する名前空間を選択します。",
|
||||
nacosSearchNamespaces: "名前空間を検索...",
|
||||
nacosSelectedNamespaces: "{selected}/{total} 件を選択",
|
||||
nacosSelectAll: "すべて選択",
|
||||
nacosClearSelection: "クリア",
|
||||
nacosShowAll: "すべて表示",
|
||||
nacosNamespaceSelectionRequired: "少なくとも 1 つ選択するか、「すべて表示」でフィルターを解除してください。",
|
||||
nacosLoadNamespacesFailed: "名前空間の読み込みに失敗しました: {message}",
|
||||
capabilityReadOnly: "この接続は読み取り専用です。",
|
||||
capabilityReadOnlyWrite: "この実装は読み取り操作のみ提供します。書き込み互換性は検証されていません。",
|
||||
capabilityVersionUnsupported: "この Nacos バージョンは操作をサポートしていません。",
|
||||
capabilityEndpointUnavailable: "この接続には管理エンドポイントがありません。",
|
||||
capabilityNotVerified: "この操作は現在の実装で検証されていません。",
|
||||
capabilityHeaderReadOnly: "読み取り専用接続のため、サービスとインスタンスの書き込みは無効です。",
|
||||
protectionTriggeredDescription: "健全なインスタンスの割合が設定したしきい値を下回ったため、保護モードが有効です。Nacos は空の一覧ではなく不健全なインスタンスを返す場合があります。",
|
||||
instanceEphemeral: "一時インスタンス",
|
||||
instancePersistent: "永続インスタンス",
|
||||
instanceUnknown: "不明なインスタンスタイプ",
|
||||
instanceConfirmDetails: "名前空間: {namespace}\nサービス: {service}\nアドレス: {ip}:{port}\nクラスター: {cluster}\n種類: {type}",
|
||||
serviceUpdateUnconfirmed: "サービス更新は送信されましたが、Nacos は対象状態をまだ返していません。サービス詳細を更新して確認してください。",
|
||||
invalidWeight: "重みは 0 以上の有限数である必要があります。",
|
||||
invalidWeightInput: "0 以上の有限な重みを入力してください。",
|
||||
metadataLabel: "メタデータ",
|
||||
selectorLabel: "セレクター",
|
||||
instanceUpdateUnconfirmed: "インスタンス更新は送信されましたが、Nacos は新しい状態をまだ返していません。更新してサーバーの状態を確認してください。",
|
||||
instanceRegisterUnconfirmed: "インスタンス登録は送信されましたが、Nacos はまだインスタンスを返していません。更新して確認してください。",
|
||||
instanceDeregisterUnconfirmed: "インスタンス解除は送信されましたが、Nacos はまだインスタンスを返しています。更新して確認してください。",
|
||||
serviceValidation: "サービス名とグループは必須です。保護しきい値は 0~1 の範囲で指定してください。",
|
||||
serviceHasInstances: "サービスにまだインスタンスがあるため削除できません。",
|
||||
serviceDeleteUnconfirmed: "サービス削除は送信されましたが、Nacos はまだサービスを返しています。再読み込みして確認してください。",
|
||||
instanceValidation: "有効な IP、ポート、0 以上の重みを入力してください。",
|
||||
protectionTriggered: "保護モードが有効",
|
||||
loadedInstances: "{count} 件のインスタンスを読み込み済み",
|
||||
filterInstanceCluster: "インスタンスのクラスターを絞り込み",
|
||||
filter: "絞り込み",
|
||||
clear: "クリア",
|
||||
serviceSettings: "サービス設定",
|
||||
serviceDetails: "サービス詳細",
|
||||
expand: "展開",
|
||||
instanceStat: "インスタンス",
|
||||
healthyInstances: "健全なインスタンス",
|
||||
clusterCount: "クラスター",
|
||||
protectThreshold: "保護しきい値",
|
||||
itemCount: "{count} 件",
|
||||
temporaryService: "一時サービス",
|
||||
loadingServiceDetail: "サービス詳細を読み込み中…",
|
||||
refreshDetail: "詳細を更新",
|
||||
noMetadata: "メタデータなし",
|
||||
noInstances: "インスタンスなし",
|
||||
createNacosService: "Nacos サービスを作成",
|
||||
editNacosService: "Nacos サービスを編集",
|
||||
manageServiceMetadata: "サービスのメタデータと保護しきい値を管理します。",
|
||||
serviceName: "サービス名",
|
||||
groupName: "グループ",
|
||||
thresholdExample: "例: 0.5",
|
||||
jsonObject: "JSON オブジェクト",
|
||||
optionalJsonObject: "JSON オブジェクト(任意)",
|
||||
jsonObjectValidation: " は JSON オブジェクトである必要があります。",
|
||||
registerPersistentInstance: "永続インスタンスを登録",
|
||||
persistentInstanceHint: "DBX はハートビートを維持できないため、一時インスタンスを登録しません。",
|
||||
ipAddress: "IP アドレス",
|
||||
port: "ポート",
|
||||
instanceEditorTitle: "インスタンスを編集",
|
||||
deleteNacosService: "Nacos サービスを削除",
|
||||
confirmEmptyServiceDelete: "この空のサービスを削除しますか?この操作は元に戻せません。",
|
||||
deregisterNacosInstance: "Nacos インスタンスを解除",
|
||||
confirmInstanceDeregister: "現在のサービスからこのインスタンスを解除しますか?",
|
||||
edit: "編集",
|
||||
restore: "元に戻す",
|
||||
registerInstance: "インスタンスを登録",
|
||||
deregister: "解除",
|
||||
},
|
||||
gridfsBrowser: {
|
||||
bucketCount: "バケット",
|
||||
|
|
|
|||
|
|
@ -6530,5 +6530,98 @@ export default withEnglishFallback({
|
|||
next: "다음",
|
||||
test: "테스트",
|
||||
readOnly: "읽기 전용",
|
||||
nacosConnectionPlan: "연결 방식",
|
||||
nacosConnectionPlanDescription: "서비스 구현과 API 버전을 선택합니다.",
|
||||
nacosServiceAddress: "서비스 주소",
|
||||
nacosServiceAddressHint: "서비스 주소를 입력하세요. 기본 포트는 8848입니다(예: http://host:8848/nacos).",
|
||||
nacosAuth: "인증",
|
||||
nacosAuthHint: "이 자격 증명은 연결 테스트와 일상적인 관리에 사용됩니다.",
|
||||
nacosUsernamePassword: "사용자 이름 / 비밀번호",
|
||||
nacosAdvancedHint: "추가 연결 옵션은 고급 탭에서 설정할 수 있습니다.",
|
||||
nacosAdvancedHintDescription: "Prometheus 메트릭, 페이지 크기, TLS 검증 및 r-nacos 확장을 설정합니다.",
|
||||
nacosGoAdvanced: "고급으로 이동",
|
||||
nacosV3ConsolePortWarning: "8080은 Nacos 3 Web Console의 기본 포트입니다. 서비스 관리는 8848 포트의 Server / Admin API 주소(예: http://127.0.0.1:8848/nacos)를 사용하세요.",
|
||||
nacosAdvancedTitle: "Nacos 고급 연결 옵션",
|
||||
nacosAdvancedDescription: "안전한 기본값이 적용되어 있으며, 사용자 지정 배포·메트릭·r-nacos 확장을 사용할 때만 조정하면 됩니다.",
|
||||
nacosPageSizeHint: "구성 및 서비스 목록에 사용되며 페이지당 1~500개입니다.",
|
||||
nacosMetricsHint: "서비스 주소에서 메트릭을 자동으로 조회합니다. 메트릭 엔드포인트가 다르면 사용자 지정 URL을 선택하세요.",
|
||||
nacosRnacosExtension: "r-nacos 확장",
|
||||
nacosRnacosExtensionHint: "구성 이력·설명 조회와 비활성 인스턴스 관리를 활성화합니다.",
|
||||
nacosEnabled: "활성화됨",
|
||||
nacosRnacosConsoleUrlHint: "일반적으로 http://host:10848입니다.",
|
||||
nacosRnacosDisabledHint: "비활성화하면 서비스 주소만 사용하며, 기본 구성 및 서비스 관리는 계속 사용할 수 있습니다.",
|
||||
nacosTlsHint: "신뢰할 수 있는 자체 서명 HTTPS 인증서에만 활성화하세요.",
|
||||
nacosVisibleNamespacesTitle: "표시할 네임스페이스 선택",
|
||||
nacosVisibleNamespacesDescription: "{name}의 사이드바에 표시할 네임스페이스를 선택합니다.",
|
||||
nacosSearchNamespaces: "네임스페이스 검색...",
|
||||
nacosSelectedNamespaces: "{selected}/{total}개 선택됨",
|
||||
nacosSelectAll: "모두 선택",
|
||||
nacosClearSelection: "지우기",
|
||||
nacosShowAll: "모두 표시",
|
||||
nacosNamespaceSelectionRequired: "하나 이상 선택하거나 “모두 표시”로 필터를 해제하세요.",
|
||||
nacosLoadNamespacesFailed: "네임스페이스를 불러오지 못했습니다: {message}",
|
||||
capabilityReadOnly: "이 연결은 읽기 전용입니다.",
|
||||
capabilityReadOnlyWrite: "이 구현은 읽기 작업만 제공하며 쓰기 호환성은 확인되지 않았습니다.",
|
||||
capabilityVersionUnsupported: "이 Nacos 버전은 해당 작업을 지원하지 않습니다.",
|
||||
capabilityEndpointUnavailable: "이 연결에는 관리 엔드포인트가 없습니다.",
|
||||
capabilityNotVerified: "현재 구현에서 이 작업이 검증되지 않았습니다.",
|
||||
capabilityHeaderReadOnly: "읽기 전용 연결이므로 서비스 및 인스턴스 쓰기가 비활성화되었습니다.",
|
||||
protectionTriggeredDescription: "정상 인스턴스 비율이 설정된 보호 임계값보다 낮아 보호 모드가 활성화되었습니다. Nacos는 빈 목록 대신 비정상 인스턴스를 반환할 수 있습니다.",
|
||||
instanceEphemeral: "임시 인스턴스",
|
||||
instancePersistent: "영구 인스턴스",
|
||||
instanceUnknown: "알 수 없는 인스턴스 유형",
|
||||
instanceConfirmDetails: "네임스페이스: {namespace}\n서비스: {service}\n주소: {ip}:{port}\n클러스터: {cluster}\n유형: {type}",
|
||||
serviceUpdateUnconfirmed: "서비스 업데이트를 제출했지만 Nacos가 대상 상태를 아직 반환하지 않았습니다. 서비스 세부 정보를 새로 고쳐 확인하세요.",
|
||||
invalidWeight: "가중치는 0 이상인 유한한 숫자여야 합니다.",
|
||||
invalidWeightInput: "0 이상인 유한한 가중치를 입력하세요.",
|
||||
metadataLabel: "메타데이터",
|
||||
selectorLabel: "셀렉터",
|
||||
instanceUpdateUnconfirmed: "인스턴스 업데이트를 제출했지만 Nacos가 새 상태를 아직 반환하지 않았습니다. 새로 고쳐 서버 상태를 확인하세요.",
|
||||
instanceRegisterUnconfirmed: "인스턴스 등록을 제출했지만 Nacos가 인스턴스를 아직 반환하지 않았습니다. 새로 고쳐 확인하세요.",
|
||||
instanceDeregisterUnconfirmed: "인스턴스 등록 해제를 제출했지만 Nacos가 아직 인스턴스를 반환합니다. 새로 고쳐 확인하세요.",
|
||||
serviceValidation: "서비스 이름과 그룹은 필수이며 보호 임계값은 0~1 사이여야 합니다.",
|
||||
serviceHasInstances: "서비스에 아직 인스턴스가 있어 삭제할 수 없습니다.",
|
||||
serviceDeleteUnconfirmed: "서비스 삭제를 제출했지만 Nacos가 아직 서비스를 반환합니다. 다시 불러와 확인하세요.",
|
||||
instanceValidation: "유효한 IP, 포트 및 음수가 아닌 가중치를 입력하세요.",
|
||||
protectionTriggered: "보호 모드 활성화",
|
||||
loadedInstances: "{count}개 인스턴스 로드됨",
|
||||
filterInstanceCluster: "인스턴스 클러스터 필터",
|
||||
filter: "필터",
|
||||
clear: "지우기",
|
||||
serviceSettings: "서비스 설정",
|
||||
serviceDetails: "서비스 세부 정보",
|
||||
expand: "펼치기",
|
||||
instanceStat: "인스턴스",
|
||||
healthyInstances: "정상 인스턴스",
|
||||
clusterCount: "클러스터",
|
||||
protectThreshold: "보호 임계값",
|
||||
itemCount: "{count}개 항목",
|
||||
temporaryService: "임시 서비스",
|
||||
loadingServiceDetail: "서비스 세부 정보 로드 중…",
|
||||
refreshDetail: "세부 정보 새로 고침",
|
||||
noMetadata: "메타데이터 없음",
|
||||
noInstances: "인스턴스 없음",
|
||||
createNacosService: "Nacos 서비스 만들기",
|
||||
editNacosService: "Nacos 서비스 편집",
|
||||
manageServiceMetadata: "서비스 메타데이터와 보호 임계값을 관리합니다.",
|
||||
serviceName: "서비스 이름",
|
||||
groupName: "그룹",
|
||||
thresholdExample: "예: 0.5",
|
||||
jsonObject: "JSON 객체",
|
||||
optionalJsonObject: "JSON 객체(선택 사항)",
|
||||
jsonObjectValidation: " JSON 객체여야 합니다.",
|
||||
registerPersistentInstance: "영구 인스턴스 등록",
|
||||
persistentInstanceHint: "DBX는 하트비트를 유지할 수 없으므로 임시 인스턴스를 등록하지 않습니다.",
|
||||
ipAddress: "IP 주소",
|
||||
port: "포트",
|
||||
instanceEditorTitle: "인스턴스 편집",
|
||||
deleteNacosService: "Nacos 서비스 삭제",
|
||||
confirmEmptyServiceDelete: "이 빈 서비스를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||
deregisterNacosInstance: "Nacos 인스턴스 등록 해제",
|
||||
confirmInstanceDeregister: "현재 서비스에서 이 인스턴스의 등록을 해제하시겠습니까?",
|
||||
edit: "편집",
|
||||
restore: "복원",
|
||||
registerInstance: "인스턴스 등록",
|
||||
deregister: "등록 해제",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5732,6 +5732,99 @@ export default withEnglishFallback({
|
|||
next: "Próximo",
|
||||
test: "Testar",
|
||||
readOnly: "Somente leitura",
|
||||
nacosConnectionPlan: "Plano de conexão",
|
||||
nacosConnectionPlanDescription: "Escolha a implementação do serviço e a versão da API.",
|
||||
nacosServiceAddress: "Endereço do serviço",
|
||||
nacosServiceAddressHint: "Informe o endereço do serviço. A porta padrão é 8848, por exemplo http://host:8848/nacos.",
|
||||
nacosAuth: "Autenticação",
|
||||
nacosAuthHint: "Essas credenciais são usadas nos testes de conexão e no gerenciamento diário.",
|
||||
nacosUsernamePassword: "Usuário / senha",
|
||||
nacosAdvancedHint: "Mais opções de conexão estão disponíveis na aba Avançado.",
|
||||
nacosAdvancedHintDescription: "Configure ali as métricas do Prometheus, o tamanho da página, a validação TLS e as extensões do r-nacos.",
|
||||
nacosGoAdvanced: "Ir para Avançado",
|
||||
nacosV3ConsolePortWarning: "8080 é a porta padrão do Web Console do Nacos 3. Para gerenciar serviços, use o endereço Server / Admin API na porta 8848, por exemplo http://127.0.0.1:8848/nacos.",
|
||||
nacosAdvancedTitle: "Opções avançadas de conexão do Nacos",
|
||||
nacosAdvancedDescription: "Esses parâmetros têm valores seguros e normalmente só precisam ser ajustados para implantações personalizadas, métricas ou extensões do r-nacos.",
|
||||
nacosPageSizeHint: "Usado nas listas de configurações e serviços; de 1 a 500 itens por página.",
|
||||
nacosMetricsHint: "Consulta as métricas automaticamente pelo endereço do serviço; escolha uma URL personalizada se o endpoint for diferente.",
|
||||
nacosRnacosExtension: "Extensões do r-nacos",
|
||||
nacosRnacosExtensionHint: "Ativa o histórico e as descrições de configurações e o gerenciamento de instâncias desativadas.",
|
||||
nacosEnabled: "Ativado",
|
||||
nacosRnacosConsoleUrlHint: "Normalmente http://host:10848.",
|
||||
nacosRnacosDisabledHint: "Quando desativado, somente o endereço do serviço é usado; a configuração e o gerenciamento básico de serviços continuam disponíveis.",
|
||||
nacosTlsHint: "Ative somente para um certificado HTTPS autoassinado confiável.",
|
||||
nacosVisibleNamespacesTitle: "Escolher namespaces visíveis",
|
||||
nacosVisibleNamespacesDescription: "Escolha os namespaces que serão exibidos na barra lateral de {name}.",
|
||||
nacosSearchNamespaces: "Pesquisar namespaces...",
|
||||
nacosSelectedNamespaces: "{selected}/{total} selecionados",
|
||||
nacosSelectAll: "Selecionar tudo",
|
||||
nacosClearSelection: "Limpar",
|
||||
nacosShowAll: "Mostrar tudo",
|
||||
nacosNamespaceSelectionRequired: "Selecione pelo menos um namespace ou use “Mostrar tudo” para limpar o filtro.",
|
||||
nacosLoadNamespacesFailed: "Falha ao carregar namespaces: {message}",
|
||||
capabilityReadOnly: "Esta conexão é somente leitura.",
|
||||
capabilityReadOnlyWrite: "Esta implementação oferece apenas operações de leitura; a compatibilidade de escrita não foi verificada.",
|
||||
capabilityVersionUnsupported: "Esta versão do Nacos não oferece suporte à operação.",
|
||||
capabilityEndpointUnavailable: "Esta conexão não fornece o endpoint de gerenciamento.",
|
||||
capabilityNotVerified: "Esta operação não foi verificada para a implementação atual.",
|
||||
capabilityHeaderReadOnly: "Esta conexão é somente leitura; as gravações de serviços e instâncias estão desativadas.",
|
||||
protectionTriggeredDescription: "O modo de proteção foi ativado porque a proporção de instâncias saudáveis está abaixo do limite configurado. O Nacos ainda pode retornar instâncias não saudáveis em vez de uma lista vazia.",
|
||||
instanceEphemeral: "Instância efêmera",
|
||||
instancePersistent: "Instância persistente",
|
||||
instanceUnknown: "Tipo de instância desconhecido",
|
||||
instanceConfirmDetails: "Namespace: {namespace}\nServiço: {service}\nEndereço: {ip}:{port}\nCluster: {cluster}\nTipo: {type}",
|
||||
serviceUpdateUnconfirmed: "A atualização do serviço foi enviada, mas o Nacos ainda não retornou o estado esperado. Atualize os detalhes do serviço para confirmar.",
|
||||
invalidWeight: "O peso deve ser um número finito maior ou igual a 0.",
|
||||
invalidWeightInput: "Informe um peso finito maior ou igual a 0.",
|
||||
metadataLabel: "Metadados",
|
||||
selectorLabel: "Seletor",
|
||||
instanceUpdateUnconfirmed: "A atualização da instância foi enviada, mas o Nacos ainda não retornou o novo estado. Atualize para verificar o estado do servidor.",
|
||||
instanceRegisterUnconfirmed: "O registro da instância foi enviado, mas o Nacos ainda não retornou a instância. Atualize para confirmar.",
|
||||
instanceDeregisterUnconfirmed: "A remoção da instância foi enviada, mas o Nacos ainda retorna a instância. Atualize para confirmar.",
|
||||
serviceValidation: "O nome do serviço e o grupo são obrigatórios; o limite de proteção deve estar entre 0 e 1.",
|
||||
serviceHasInstances: "O serviço ainda contém instâncias e não pode ser excluído.",
|
||||
serviceDeleteUnconfirmed: "A exclusão do serviço foi enviada, mas o Nacos ainda retorna o serviço. Recarregue e confirme.",
|
||||
instanceValidation: "Informe um IP, uma porta e um peso não negativo válidos.",
|
||||
protectionTriggered: "Modo de proteção ativado",
|
||||
loadedInstances: "{count} instâncias carregadas",
|
||||
filterInstanceCluster: "Filtrar cluster da instância",
|
||||
filter: "Filtrar",
|
||||
clear: "Limpar",
|
||||
serviceSettings: "Configurações do serviço",
|
||||
serviceDetails: "Detalhes do serviço",
|
||||
expand: "Expandir",
|
||||
instanceStat: "Instâncias",
|
||||
healthyInstances: "Instâncias saudáveis",
|
||||
clusterCount: "Clusters",
|
||||
protectThreshold: "Limite de proteção",
|
||||
itemCount: "{count} itens",
|
||||
temporaryService: "Serviço efêmero",
|
||||
loadingServiceDetail: "Carregando detalhes do serviço…",
|
||||
refreshDetail: "Atualizar detalhes",
|
||||
noMetadata: "Sem metadados",
|
||||
noInstances: "Nenhuma instância",
|
||||
createNacosService: "Criar serviço Nacos",
|
||||
editNacosService: "Editar serviço Nacos",
|
||||
manageServiceMetadata: "Gerencie os metadados e o limite de proteção do serviço.",
|
||||
serviceName: "Nome do serviço",
|
||||
groupName: "Grupo",
|
||||
thresholdExample: "Por exemplo, 0,5",
|
||||
jsonObject: "Objeto JSON",
|
||||
optionalJsonObject: "Objeto JSON, opcional",
|
||||
jsonObjectValidation: " deve ser um objeto JSON.",
|
||||
registerPersistentInstance: "Registrar instância persistente",
|
||||
persistentInstanceHint: "O DBX não registra instâncias efêmeras porque não consegue manter seus heartbeats.",
|
||||
ipAddress: "Endereço IP",
|
||||
port: "Porta",
|
||||
instanceEditorTitle: "Editar instância",
|
||||
deleteNacosService: "Excluir serviço Nacos",
|
||||
confirmEmptyServiceDelete: "Excluir este serviço vazio? Essa ação não pode ser desfeita.",
|
||||
deregisterNacosInstance: "Remover instância Nacos",
|
||||
confirmInstanceDeregister: "Remover esta instância do serviço atual?",
|
||||
edit: "Editar",
|
||||
restore: "Restaurar",
|
||||
registerInstance: "Registrar instância",
|
||||
deregister: "Remover registro",
|
||||
},
|
||||
gridfsBrowser: {
|
||||
bucketCount: "Buckets",
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ export default withEnglishFallback({
|
|||
nacosConfigurationHistoryHint: "r-nacos 的配置历史仅通过独立控制台提供(通常为 10848 端口)。控制台与兼容 Nacos 的 OpenAPI 使用不同的登录会话:OpenAPI 可以不启用认证,而控制台仍需登录。通常填写同一套 r-nacos 用户凭据;仅在需要使用另一账号时单独设置。",
|
||||
nacosRNacosConsoleUrl: "r-nacos 控制台 URL",
|
||||
nacosRNacosConsoleUrlPlaceholder: "http://127.0.0.1:10848",
|
||||
nacosRNacosConsoleUrlHint: "可选。使用独立控制台服务地址(通常为 10848),不是 OpenAPI 地址;它用于配置历史,以及读取 r-nacos 配置的类型和描述。",
|
||||
nacosRNacosConsoleUrlHint: "可选。使用独立控制台服务地址(通常为 10848),不是 OpenAPI 地址;它用于配置历史、读取 r-nacos 配置的类型和描述,以及在服务管理中展示已禁用实例。",
|
||||
nacosRNacosOpenApiRequired: "r-nacos 主地址必须使用兼容 Nacos 的 API 地址,不能使用独立控制台 URL。",
|
||||
nacosRNacosConsoleUrlRequired: "启用 r-nacos 配置历史时必须填写控制台 URL。",
|
||||
nacosConsoleAuthSeparateRequired: "主连接未启用认证;请单独设置控制台凭据。",
|
||||
|
|
@ -6920,7 +6920,7 @@ export default withEnglishFallback({
|
|||
historyConsoleUrlMissing: "配置历史需要填写 r-nacos 控制台地址。",
|
||||
historyConsoleCredentialsMissing: "配置历史需要填写 r-nacos 控制台凭据。",
|
||||
rnacosConsoleAuthTitle: "r-nacos 控制台验证",
|
||||
rnacosConsoleAuthDescription: "请输入 r-nacos 控制台验证码,以读取配置类型、描述和配置历史。",
|
||||
rnacosConsoleAuthDescription: "请输入 r-nacos 控制台验证码,以读取配置类型、描述、配置历史和完整服务实例列表(含已禁用实例)。",
|
||||
rnacosCaptchaLabel: "验证码",
|
||||
rnacosCaptchaPlaceholder: "输入上图中的验证码",
|
||||
rnacosCaptchaRequired: "请输入验证码。",
|
||||
|
|
@ -7002,5 +7002,98 @@ export default withEnglishFallback({
|
|||
next: "下一页",
|
||||
test: "测试",
|
||||
readOnly: "只读",
|
||||
nacosConnectionPlan: "连接方案",
|
||||
nacosConnectionPlanDescription: "选择服务实现和 API 版本。",
|
||||
nacosServiceAddress: "服务地址",
|
||||
nacosServiceAddressHint: "填写服务地址即可,默认端口为 8848,例如 http://主机:8848/nacos。",
|
||||
nacosAuth: "认证",
|
||||
nacosAuthHint: "认证信息同时用于连接测试和日常管理。",
|
||||
nacosUsernamePassword: "账号密码",
|
||||
nacosAdvancedHint: "更多连接选项位于“高级”页",
|
||||
nacosAdvancedHintDescription: "可在高级页配置 Prometheus 指标、分页大小、TLS 证书校验和 r-nacos 扩展。",
|
||||
nacosGoAdvanced: "前往高级",
|
||||
nacosV3ConsolePortWarning: "8080 是 Nacos 3 默认 Web 控制台端口。服务管理请使用 8848 端口的 Server / Admin API 地址,例如 http://127.0.0.1:8848/nacos。",
|
||||
nacosAdvancedTitle: "Nacos 高级连接选项",
|
||||
nacosAdvancedDescription: "这些参数均有安全默认值,通常只有自定义部署、指标采集或 r-nacos 扩展能力需要调整。",
|
||||
nacosPageSizeHint: "用于配置和服务列表,每页 1–500 条。",
|
||||
nacosMetricsHint: "自动使用服务地址查询指标;若指标端点不同,请选择“自定义地址”。",
|
||||
nacosRnacosExtension: "r-nacos 扩展控制台",
|
||||
nacosRnacosExtensionHint: "启用后可使用配置历史、配置描述和禁用实例管理。",
|
||||
nacosEnabled: "启用",
|
||||
nacosRnacosConsoleUrlHint: "通常为 http://主机:10848。",
|
||||
nacosRnacosDisabledHint: "未启用时仅使用服务地址,基础配置和服务管理仍可用。",
|
||||
nacosTlsHint: "仅在使用自签名 HTTPS 证书且确认目标可信时开启。",
|
||||
nacosVisibleNamespacesTitle: "选择显示的命名空间",
|
||||
nacosVisibleNamespacesDescription: "选择「{name}」下要在侧边栏显示的命名空间。",
|
||||
nacosSearchNamespaces: "搜索命名空间...",
|
||||
nacosSelectedNamespaces: "已选择 {selected}/{total}",
|
||||
nacosSelectAll: "全选",
|
||||
nacosClearSelection: "清空",
|
||||
nacosShowAll: "显示全部",
|
||||
nacosNamespaceSelectionRequired: "至少选择一个命名空间,或使用“显示全部”清除过滤。",
|
||||
nacosLoadNamespacesFailed: "加载命名空间失败:{message}",
|
||||
capabilityReadOnly: "当前连接为只读连接。",
|
||||
capabilityReadOnlyWrite: "当前服务实现仅开放读取能力,写操作尚未验证兼容。",
|
||||
capabilityVersionUnsupported: "当前 Nacos 版本不支持此操作。",
|
||||
capabilityEndpointUnavailable: "当前连接未提供此管理接口。",
|
||||
capabilityNotVerified: "此操作尚未在当前服务实现中验证。",
|
||||
capabilityHeaderReadOnly: "当前连接为只读连接,服务和实例写操作已禁用。",
|
||||
protectionTriggeredDescription: "保护模式已触发:健康实例比例低于服务设置的保护阈值。为避免客户端拿到空实例列表,Nacos 仍可能返回不健康实例。",
|
||||
instanceEphemeral: "临时实例",
|
||||
instancePersistent: "持久实例",
|
||||
instanceUnknown: "实例类型未知",
|
||||
instanceConfirmDetails: "命名空间:{namespace}\n服务:{service}\n地址:{ip}:{port}\n集群:{cluster}\n类型:{type}",
|
||||
serviceUpdateUnconfirmed: "服务更新已提交,但 Nacos 尚未返回目标状态。请刷新服务详情后确认。",
|
||||
invalidWeight: "实例权重必须是大于或等于 0 的有限数字。",
|
||||
invalidWeightInput: "请输入大于或等于 0 的有限权重。",
|
||||
metadataLabel: "元数据",
|
||||
selectorLabel: "选择器",
|
||||
instanceUpdateUnconfirmed: "实例更新已提交,但 Nacos 尚未返回新状态。请刷新后查看服务端状态。",
|
||||
instanceRegisterUnconfirmed: "实例注册已提交,但 Nacos 尚未返回该实例。请刷新后确认。",
|
||||
instanceDeregisterUnconfirmed: "实例注销已提交,但 Nacos 仍返回该实例。请刷新后确认。",
|
||||
serviceValidation: "服务名称和分组不能为空,保护阈值必须在 0 到 1 之间。",
|
||||
serviceHasInstances: "服务仍包含实例,无法删除。",
|
||||
serviceDeleteUnconfirmed: "服务删除已提交,但 Nacos 仍返回该服务。请重新加载后确认。",
|
||||
instanceValidation: "请输入有效的 IP、端口和非负权重。",
|
||||
protectionTriggered: "保护模式已触发",
|
||||
loadedInstances: "{count} 个已加载实例",
|
||||
filterInstanceCluster: "筛选实例集群",
|
||||
filter: "筛选",
|
||||
clear: "清除",
|
||||
serviceSettings: "服务设置",
|
||||
serviceDetails: "服务详情",
|
||||
expand: "展开",
|
||||
instanceStat: "实例",
|
||||
healthyInstances: "健康实例",
|
||||
clusterCount: "集群",
|
||||
protectThreshold: "保护阈值",
|
||||
itemCount: "{count} 项",
|
||||
temporaryService: "临时服务",
|
||||
loadingServiceDetail: "正在加载服务详情…",
|
||||
refreshDetail: "刷新详情",
|
||||
noMetadata: "无元数据",
|
||||
noInstances: "暂无实例",
|
||||
createNacosService: "创建 Nacos 服务",
|
||||
editNacosService: "编辑 Nacos 服务",
|
||||
manageServiceMetadata: "管理服务元数据和保护阈值。",
|
||||
serviceName: "服务名称",
|
||||
groupName: "分组",
|
||||
thresholdExample: "例如 0.5",
|
||||
jsonObject: "JSON 对象",
|
||||
optionalJsonObject: "JSON 对象,可选",
|
||||
jsonObjectValidation: "必须是 JSON 对象。",
|
||||
registerPersistentInstance: "注册持久实例",
|
||||
persistentInstanceHint: "DBX 不注册临时实例,因为无法为其提供心跳维持。",
|
||||
ipAddress: "IP 地址",
|
||||
port: "端口",
|
||||
instanceEditorTitle: "编辑实例",
|
||||
deleteNacosService: "删除 Nacos 服务",
|
||||
confirmEmptyServiceDelete: "确认删除这个空服务吗?此操作不可撤销。",
|
||||
deregisterNacosInstance: "注销 Nacos 实例",
|
||||
confirmInstanceDeregister: "确认从当前服务中注销这个实例吗?",
|
||||
edit: "编辑",
|
||||
restore: "还原",
|
||||
registerInstance: "注册实例",
|
||||
deregister: "注销",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5195,6 +5195,99 @@ export default withEnglishFallback({
|
|||
namespaceNamePlaceholder: "輸入命名空間名稱",
|
||||
namespaceDesc: "描述",
|
||||
namespaceDescPlaceholder: "預設為命名空間名稱",
|
||||
nacosConnectionPlan: "連線方案",
|
||||
nacosConnectionPlanDescription: "選擇服務實作與 API 版本。",
|
||||
nacosServiceAddress: "服務位址",
|
||||
nacosServiceAddressHint: "填寫服務位址即可,預設連接埠為 8848,例如 http://host:8848/nacos。",
|
||||
nacosAuth: "驗證",
|
||||
nacosAuthHint: "這些驗證資訊會用於連線測試與日常管理。",
|
||||
nacosUsernamePassword: "使用者名稱 / 密碼",
|
||||
nacosAdvancedHint: "更多連線選項位於「進階」頁。",
|
||||
nacosAdvancedHintDescription: "可在此設定 Prometheus 指標、分頁大小、TLS 驗證與 r-nacos 擴充功能。",
|
||||
nacosGoAdvanced: "前往進階",
|
||||
nacosV3ConsolePortWarning: "8080 是 Nacos 3 Web Console 的預設連接埠。服務管理請使用 8848 連接埠的 Server / Admin API 位址,例如 http://127.0.0.1:8848/nacos。",
|
||||
nacosAdvancedTitle: "Nacos 進階連線選項",
|
||||
nacosAdvancedDescription: "這些參數都有安全的預設值,通常只有自訂部署、指標或 r-nacos 擴充功能需要調整。",
|
||||
nacosPageSizeHint: "用於設定與服務清單,每頁 1–500 筆。",
|
||||
nacosMetricsHint: "自動使用服務位址查詢指標;若指標端點不同,請選擇自訂 URL。",
|
||||
nacosRnacosExtension: "r-nacos 擴充功能",
|
||||
nacosRnacosExtensionHint: "啟用設定歷史、設定描述與停用實例管理。",
|
||||
nacosEnabled: "啟用",
|
||||
nacosRnacosConsoleUrlHint: "通常為 http://host:10848。",
|
||||
nacosRnacosDisabledHint: "停用時只使用服務位址;基本設定與服務管理仍可使用。",
|
||||
nacosTlsHint: "僅在使用可信任的自簽 HTTPS 憑證時啟用。",
|
||||
nacosVisibleNamespacesTitle: "選擇顯示的命名空間",
|
||||
nacosVisibleNamespacesDescription: "選擇要在「{name}」側邊欄顯示的命名空間。",
|
||||
nacosSearchNamespaces: "搜尋命名空間...",
|
||||
nacosSelectedNamespaces: "已選 {selected}/{total}",
|
||||
nacosSelectAll: "全選",
|
||||
nacosClearSelection: "清除",
|
||||
nacosShowAll: "顯示全部",
|
||||
nacosNamespaceSelectionRequired: "至少選擇一個命名空間,或使用「顯示全部」清除篩選。",
|
||||
nacosLoadNamespacesFailed: "載入命名空間失敗:{message}",
|
||||
capabilityReadOnly: "此連線為唯讀連線。",
|
||||
capabilityReadOnlyWrite: "目前實作僅提供讀取操作,寫入相容性尚未驗證。",
|
||||
capabilityVersionUnsupported: "此 Nacos 版本不支援此操作。",
|
||||
capabilityEndpointUnavailable: "此連線未提供管理端點。",
|
||||
capabilityNotVerified: "此操作尚未在目前實作中驗證。",
|
||||
capabilityHeaderReadOnly: "此連線為唯讀連線,服務與實例寫入操作已停用。",
|
||||
protectionTriggeredDescription: "健康實例比例低於設定的保護閾值,因此已觸發保護模式。Nacos 仍可能回傳不健康實例,而非空清單。",
|
||||
instanceEphemeral: "臨時實例",
|
||||
instancePersistent: "持久實例",
|
||||
instanceUnknown: "未知實例類型",
|
||||
instanceConfirmDetails: "命名空間:{namespace}\n服務:{service}\n位址:{ip}:{port}\n叢集:{cluster}\n類型:{type}",
|
||||
serviceUpdateUnconfirmed: "服務更新已送出,但 Nacos 尚未回傳目標狀態。請重新整理服務詳細資料以確認。",
|
||||
invalidWeight: "權重必須是大於或等於 0 的有限數字。",
|
||||
invalidWeightInput: "請輸入大於或等於 0 的有限權重。",
|
||||
metadataLabel: "中繼資料",
|
||||
selectorLabel: "選取器",
|
||||
instanceUpdateUnconfirmed: "實例更新已送出,但 Nacos 尚未回傳新狀態。請重新整理以查看伺服器狀態。",
|
||||
instanceRegisterUnconfirmed: "實例註冊已送出,但 Nacos 尚未回傳該實例。請重新整理確認。",
|
||||
instanceDeregisterUnconfirmed: "實例註銷已送出,但 Nacos 仍回傳該實例。請重新整理確認。",
|
||||
serviceValidation: "服務名稱和群組為必填,保護閾值必須介於 0 到 1 之間。",
|
||||
serviceHasInstances: "服務仍包含實例,無法刪除。",
|
||||
serviceDeleteUnconfirmed: "服務刪除已送出,但 Nacos 仍回傳該服務。請重新載入確認。",
|
||||
instanceValidation: "請輸入有效的 IP、連接埠和非負權重。",
|
||||
protectionTriggered: "已觸發保護模式",
|
||||
loadedInstances: "已載入 {count} 個實例",
|
||||
filterInstanceCluster: "篩選實例叢集",
|
||||
filter: "篩選",
|
||||
clear: "清除",
|
||||
serviceSettings: "服務設定",
|
||||
serviceDetails: "服務詳細資料",
|
||||
expand: "展開",
|
||||
instanceStat: "實例",
|
||||
healthyInstances: "健康實例",
|
||||
clusterCount: "叢集",
|
||||
protectThreshold: "保護閾值",
|
||||
itemCount: "{count} 項",
|
||||
temporaryService: "臨時服務",
|
||||
loadingServiceDetail: "正在載入服務詳細資料…",
|
||||
refreshDetail: "重新整理詳細資料",
|
||||
noMetadata: "沒有中繼資料",
|
||||
noInstances: "沒有實例",
|
||||
createNacosService: "建立 Nacos 服務",
|
||||
editNacosService: "編輯 Nacos 服務",
|
||||
manageServiceMetadata: "管理服務中繼資料與保護閾值。",
|
||||
serviceName: "服務名稱",
|
||||
groupName: "群組",
|
||||
thresholdExample: "例如 0.5",
|
||||
jsonObject: "JSON 物件",
|
||||
optionalJsonObject: "JSON 物件,可選",
|
||||
jsonObjectValidation: "必須是 JSON 物件。",
|
||||
registerPersistentInstance: "註冊持久實例",
|
||||
persistentInstanceHint: "DBX 不註冊臨時實例,因為無法維持其心跳。",
|
||||
ipAddress: "IP 位址",
|
||||
port: "連接埠",
|
||||
instanceEditorTitle: "編輯實例",
|
||||
deleteNacosService: "刪除 Nacos 服務",
|
||||
confirmEmptyServiceDelete: "確定要刪除此空服務嗎?此操作無法復原。",
|
||||
deregisterNacosInstance: "註銷 Nacos 實例",
|
||||
confirmInstanceDeregister: "確定要從目前服務註銷此實例嗎?",
|
||||
edit: "編輯",
|
||||
restore: "還原",
|
||||
registerInstance: "註冊實例",
|
||||
deregister: "註銷",
|
||||
},
|
||||
userAdmin: {
|
||||
title: "使用者與權限",
|
||||
|
|
|
|||
|
|
@ -7,13 +7,19 @@ describe("connection dialog scrolling", () => {
|
|||
it("keeps every configuration tab inside a shrinkable form viewport", () => {
|
||||
expect(dialogSource).toContain("return `${widthClass} connection-dialog-content--config`;");
|
||||
expect(dialogSource.match(/<TabsContent[^>]*class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">/g)).toHaveLength(4);
|
||||
expect(dialogSource.match(/class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto/g)).toHaveLength(4);
|
||||
expect(dialogSource.match(/class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto/g)).toHaveLength(4);
|
||||
expect(dialogSource).not.toMatch(/\.connection-dialog-content--config\s*\{[\s\S]*?height:\s*min\(720px/);
|
||||
expect(dialogSource).toMatch(/@media \(max-height: 720px\)[\s\S]*?height:\s*calc\(var\(--dbx-viewport-height\) - 2rem\);/);
|
||||
expect(dialogSource).toMatch(/\.connection-dialog-content--config \.connection-form-body\s*\{[\s\S]*?align-content:\s*start;/);
|
||||
});
|
||||
|
||||
it("keeps the transport form inside a shrinkable scroll viewport", () => {
|
||||
expect(dialogSource).toContain('<TabsContent v-if="canUseTransportLayers" value="transport" class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">');
|
||||
expect(dialogSource).toContain('class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2"');
|
||||
expect(dialogSource).toContain('class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2 pb-6"');
|
||||
});
|
||||
|
||||
it("keeps conditional Nacos authentication fields from shrinking earlier cards", () => {
|
||||
expect(dialogSource).toContain(":class=\"{ 'connection-form-body--nacos': form.db_type === 'nacos' }\"");
|
||||
expect(dialogSource).toMatch(/\.connection-form-body--nacos\s*\{[\s\S]*?grid-auto-rows:\s*max-content;/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("Nacos connection dialog layout", () => {
|
||||
it("presents implementation and version as one explicit connection profile", () => {
|
||||
expect(source).toContain("data-nacos-profile-selector");
|
||||
expect(source).toContain('v-for="profile in NACOS_CONNECTION_PROFILES"');
|
||||
expect(source).toContain("selectNacosConnectionProfile(profile.value)");
|
||||
expect(source).not.toContain("nacosVersionMode = 'auto'");
|
||||
expect(source).not.toContain("tryNacosDockerConsoleFallback");
|
||||
expect(source).not.toContain("dockerNacosConsoleFallbackUrl");
|
||||
});
|
||||
|
||||
it("keeps the primary form focused on endpoints and authentication", () => {
|
||||
const mainStart = source.indexOf("data-nacos-profile-selector");
|
||||
const mainEnd = source.indexOf("<!-- Redis: host, port, user, password, ssl -->", mainStart);
|
||||
const main = source.slice(mainStart, mainEnd);
|
||||
|
||||
expect(main).toContain("data-nacos-endpoint-section");
|
||||
expect(main).toContain("data-nacos-access-section");
|
||||
expect(main).toContain("data-nacos-advanced-hint");
|
||||
expect(main).toContain('t("nacos.nacosAdvancedHint")');
|
||||
expect(main).toContain("@click=\"configTab = 'advanced'\"");
|
||||
expect(main).toContain('v-model="nacosServerAddr"');
|
||||
expect(main).not.toContain('v-model="nacosV3ConsoleAddr"');
|
||||
expect(main).not.toContain('v-model="nacosNamespace"');
|
||||
expect(main).toContain('t("nacos.nacosAuthHint")');
|
||||
expect(main).not.toContain('v-model="nacosMetricsMode"');
|
||||
expect(main).not.toContain('v-model.number="nacosPageSize"');
|
||||
});
|
||||
|
||||
it("moves low-frequency and r-nacos console settings to the advanced tab", () => {
|
||||
const advancedStart = source.indexOf("data-nacos-advanced-settings");
|
||||
const advancedEnd = source.indexOf('v-if="showGaussdbConnectionMode"', advancedStart);
|
||||
const advanced = source.slice(advancedStart, advancedEnd);
|
||||
|
||||
expect(advanced).not.toContain('v-model="nacosContextPathInput"');
|
||||
expect(advanced).not.toContain("配置上下文路径");
|
||||
expect(advanced).toContain('v-model="nacosMetricsMode"');
|
||||
expect(advanced).toContain('v-model="nacosRNacosConsoleAddr"');
|
||||
expect(advanced).toContain('v-if="nacosHistoryEnabled"');
|
||||
expect(advanced).toContain('t("nacos.nacosRnacosDisabledHint")');
|
||||
expect(advanced).toContain('v-model="nacosTlsSkipVerify"');
|
||||
expect(advanced).toContain('v-model.number="nacosPageSize"');
|
||||
});
|
||||
|
||||
it("documents product default ports instead of local Docker mappings", () => {
|
||||
expect(source).toContain('t("nacos.nacosServiceAddressHint")');
|
||||
expect(source).toContain('t("nacos.nacosMetricsHint")');
|
||||
expect(source).not.toContain("DBX 不需要配置该地址");
|
||||
const mainStart = source.indexOf("data-nacos-profile-selector");
|
||||
const mainEnd = source.indexOf("<!-- Redis: host, port, user, password, ssl -->", mainStart);
|
||||
expect(source.slice(mainStart, mainEnd)).not.toContain('placeholder="http://127.0.0.1:8080"');
|
||||
expect(source).not.toContain("http://127.0.0.1:8010");
|
||||
expect(source).not.toContain("http://127.0.0.1:8818");
|
||||
});
|
||||
|
||||
it("uses a dedicated namespace selector instead of the database selector", () => {
|
||||
expect(source).toContain('t("nacos.nacosVisibleNamespacesTitle")');
|
||||
expect(source).toContain("openVisibleNacosNamespacesPicker");
|
||||
expect(source).toContain("api.nacosListNamespaces(draftId)");
|
||||
expect(source).toContain("showVisibleNacosNamespacesDialog");
|
||||
});
|
||||
});
|
||||
|
|
@ -195,8 +195,10 @@ describe("nacosAdmin helpers", () => {
|
|||
|
||||
it("includes identifying fields in confirmations", () => {
|
||||
expect(buildNacosConfigDeleteConfirm({ namespace: "", dataId: "app.yaml", group: "DEFAULT_GROUP" })).toContain("dataId=app.yaml");
|
||||
const details = buildNacosInstanceConfirm({ serviceName: "DEFAULT_GROUP@@svc", groupName: "DEFAULT_GROUP" }, { ip: "127.0.0.1", port: 8080, enabled: true, metadata: null }, { enabled: false }, "", "public");
|
||||
const details = buildNacosInstanceConfirm({ serviceName: "DEFAULT_GROUP@@svc", groupName: "DEFAULT_GROUP" }, { ip: "127.0.0.1", port: 8080, clusterName: "blue", ephemeral: false, enabled: true, metadata: null }, { enabled: false }, "", "public");
|
||||
expect(details).toContain("serviceName=DEFAULT_GROUP@@svc");
|
||||
expect(details).toContain("cluster=blue");
|
||||
expect(details).toContain("ephemeral=false");
|
||||
expect(details).toContain("targetEnabled=false");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { filterNacosNamespacesForSidebar, normalizeNacosNamespaceSelection, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
|
||||
const namespaces = [
|
||||
{ namespace: "", namespaceShowName: "public" },
|
||||
{ namespace: "dev", namespaceShowName: "开发" },
|
||||
{ namespace: "prod", namespaceShowName: "生产" },
|
||||
];
|
||||
|
||||
describe("filterNacosNamespacesForSidebar", () => {
|
||||
it("keeps all namespaces when no visibility filter is configured", () => {
|
||||
expect(filterNacosNamespacesForSidebar(namespaces, undefined)).toEqual(namespaces);
|
||||
});
|
||||
|
||||
it("filters by namespace id and preserves the public empty identifier", () => {
|
||||
expect(filterNacosNamespacesForSidebar(namespaces, ["", "prod"])).toEqual([namespaces[0], namespaces[2]]);
|
||||
});
|
||||
|
||||
it("matches a legacy empty public selection with a concrete public namespace", () => {
|
||||
const v3Namespaces = [{ namespace: "public", namespaceShowName: "public" }, namespaces[2]];
|
||||
expect(filterNacosNamespacesForSidebar(v3Namespaces, ["", "prod"])).toEqual(v3Namespaces);
|
||||
expect(normalizeNacosNamespaceSelection([""], v3Namespaces)).toEqual(["public"]);
|
||||
});
|
||||
|
||||
it("preserves the endpoint-specific public ID when normalizing selections", () => {
|
||||
expect(normalizeNacosNamespaceSelection(["public"], namespaces)).toEqual([""]);
|
||||
});
|
||||
|
||||
it("keeps only the concrete public namespace when both legacy forms are returned", () => {
|
||||
const duplicatePublic = [namespaces[0], { namespace: "public", namespaceShowName: "public" }, namespaces[1]];
|
||||
expect(normalizeNacosNamespacesForDisplay(duplicatePublic)).toEqual([duplicatePublic[1], duplicatePublic[2]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -359,8 +359,14 @@ export const nacosRollbackConfig = forward("nacosRollbackConfig");
|
|||
export const nacosGetRNacosConsoleCaptcha = forward("nacosGetRNacosConsoleCaptcha");
|
||||
export const nacosLoginRNacosConsole = forward("nacosLoginRNacosConsole");
|
||||
export const nacosListServices = forward("nacosListServices");
|
||||
export const nacosGetService = forward("nacosGetService");
|
||||
export const nacosCreateService = forward("nacosCreateService");
|
||||
export const nacosUpdateService = forward("nacosUpdateService");
|
||||
export const nacosDeleteService = forward("nacosDeleteService");
|
||||
export const nacosListInstances = forward("nacosListInstances");
|
||||
export const nacosUpdateInstance = forward("nacosUpdateInstance");
|
||||
export const nacosRegisterInstance = forward("nacosRegisterInstance");
|
||||
export const nacosDeregisterInstance = forward("nacosDeregisterInstance");
|
||||
export const nacosGetDashboard = forward("nacosGetDashboard");
|
||||
export const nacosRawRequest = forward("nacosRawRequest");
|
||||
|
||||
|
|
|
|||
|
|
@ -183,8 +183,10 @@ import type {
|
|||
NacosConnectionInfo,
|
||||
NacosRNacosConsoleCaptcha,
|
||||
NacosInstanceInfo,
|
||||
NacosInstanceRef,
|
||||
NacosInstanceRegistration,
|
||||
NacosInstanceQuery,
|
||||
NacosInstanceUpdate,
|
||||
NacosInstanceUpdateRequest,
|
||||
NacosDashboardQuery,
|
||||
NacosDashboardSnapshot,
|
||||
NacosNamespaceCreate,
|
||||
|
|
@ -193,7 +195,9 @@ import type {
|
|||
NacosRawRequest,
|
||||
NacosRawResponse,
|
||||
NacosServiceList,
|
||||
NacosServiceDetail,
|
||||
NacosServiceQuery,
|
||||
NacosServiceUpsert,
|
||||
NacosSearchProgress,
|
||||
} from "@/types/nacos";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
|
|
@ -2806,14 +2810,38 @@ export async function nacosListServices(connectionId: string, query: NacosServic
|
|||
return post("/api/nacos/services/list", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosGetService(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceDetail> {
|
||||
return post("/api/nacos/services/get", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosCreateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return post("/api/nacos/services/create", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosUpdateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return post("/api/nacos/services/update", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeleteService(connectionId: string, query: NacosServiceQuery): Promise<void> {
|
||||
return post("/api/nacos/services/delete", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosListInstances(connectionId: string, query: NacosInstanceQuery): Promise<NacosInstanceInfo[]> {
|
||||
return post("/api/nacos/instances/list", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdate): Promise<void> {
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdateRequest): Promise<void> {
|
||||
return post("/api/nacos/instances/update", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosRegisterInstance(connectionId: string, req: NacosInstanceRegistration): Promise<void> {
|
||||
return post("/api/nacos/instances/register", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeregisterInstance(connectionId: string, req: NacosInstanceRef): Promise<void> {
|
||||
return post("/api/nacos/instances/deregister", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosGetDashboard(connectionId: string, query: NacosDashboardQuery): Promise<NacosDashboardSnapshot> {
|
||||
return post("/api/nacos/dashboard", { connectionId, query });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,15 +21,19 @@ import type {
|
|||
NacosDashboardSnapshot,
|
||||
NacosRNacosConsoleCaptcha,
|
||||
NacosInstanceInfo,
|
||||
NacosInstanceRef,
|
||||
NacosInstanceRegistration,
|
||||
NacosInstanceQuery,
|
||||
NacosInstanceUpdate,
|
||||
NacosInstanceUpdateRequest,
|
||||
NacosNamespaceCreate,
|
||||
NacosNamespaceInfo,
|
||||
NacosNamespaceUpdate,
|
||||
NacosRawRequest,
|
||||
NacosRawResponse,
|
||||
NacosServiceList,
|
||||
NacosServiceDetail,
|
||||
NacosServiceQuery,
|
||||
NacosServiceUpsert,
|
||||
NacosSearchProgress,
|
||||
} from "@/types/nacos";
|
||||
|
||||
|
|
@ -121,14 +125,38 @@ export async function nacosListServices(connectionId: string, query: NacosServic
|
|||
return invoke("nacos_list_services", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosGetService(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceDetail> {
|
||||
return invoke("nacos_get_service", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosCreateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return invoke("nacos_create_service", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosUpdateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return invoke("nacos_update_service", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeleteService(connectionId: string, query: NacosServiceQuery): Promise<void> {
|
||||
return invoke("nacos_delete_service", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosListInstances(connectionId: string, query: NacosInstanceQuery): Promise<NacosInstanceInfo[]> {
|
||||
return invoke("nacos_list_instances", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdate): Promise<void> {
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdateRequest): Promise<void> {
|
||||
return invoke("nacos_update_instance", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosRegisterInstance(connectionId: string, req: NacosInstanceRegistration): Promise<void> {
|
||||
return invoke("nacos_register_instance", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeregisterInstance(connectionId: string, req: NacosInstanceRef): Promise<void> {
|
||||
return invoke("nacos_deregister_instance", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosGetDashboard(connectionId: string, query: NacosDashboardQuery): Promise<NacosDashboardSnapshot> {
|
||||
return invoke("nacos_get_dashboard", { connectionId, query });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { nacosInstanceMatchesPatch, nacosInstanceRefIdentity, nacosIpAddressIsValid, nacosJsonObjectMatches, nacosServiceDetailMatches } from "../nacosServiceManagement";
|
||||
|
||||
describe("Nacos service management state reconciliation", () => {
|
||||
it("keeps unknown, persistent and ephemeral instances as separate identities", () => {
|
||||
const base = { serviceName: "api", ip: "127.0.0.1", port: 8080, clusterName: "blue" };
|
||||
expect(new Set([nacosInstanceRefIdentity(base), nacosInstanceRefIdentity({ ...base, ephemeral: false }), nacosInstanceRefIdentity({ ...base, ephemeral: true })]).size).toBe(3);
|
||||
});
|
||||
|
||||
it("validates IPv4 and IPv6 instance addresses without accepting host names", () => {
|
||||
expect(nacosIpAddressIsValid("127.0.0.1")).toBe(true);
|
||||
expect(nacosIpAddressIsValid("2001:db8::1")).toBe(true);
|
||||
expect(nacosIpAddressIsValid("999.0.0.1")).toBe(false);
|
||||
expect(nacosIpAddressIsValid("localhost")).toBe(false);
|
||||
});
|
||||
|
||||
it("compares metadata independently of object key order", () => {
|
||||
expect(nacosJsonObjectMatches({ owner: "dbx", nested: { b: 2, a: 1 } }, { nested: { a: 1, b: 2 }, owner: "dbx" })).toBe(true);
|
||||
});
|
||||
|
||||
it("verifies only fields present in an instance patch", () => {
|
||||
const instance = { ip: "127.0.0.1", port: 8080, weight: 0.3, enabled: false, healthy: true, metadata: { role: "api" } };
|
||||
expect(nacosInstanceMatchesPatch(instance, { weight: 0.3000000001, metadata: { role: "api" } })).toBe(true);
|
||||
expect(nacosInstanceMatchesPatch(instance, { metadata: { role: "worker" } })).toBe(false);
|
||||
expect(nacosInstanceMatchesPatch({ ip: "127.0.0.1", port: 8080 }, { weight: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("treats Nacos none selectors as the UI's unconfigured selector", () => {
|
||||
const expected = { serviceName: "api", metadata: { owner: "dbx" }, protectThreshold: 0.5 };
|
||||
expect(nacosServiceDetailMatches({ serviceName: "api", metadata: { owner: "dbx" }, protectThreshold: 0.5, selector: { type: "NoneSelector", contextType: "NONE" } }, expected)).toBe(true);
|
||||
expect(nacosServiceDetailMatches({ serviceName: "api", metadata: { owner: "dbx" } }, expected)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -12,26 +12,26 @@ export interface NacosRawTemplate {
|
|||
}
|
||||
|
||||
export const NACOS_RAW_TEMPLATES: NacosRawTemplate[] = [
|
||||
{ key: "serverState", method: "GET", path: "/v3/console/server/state", query: "", body: "" },
|
||||
{ key: "namespaceList", method: "GET", path: "/v3/console/core/namespace/list", query: "", body: "" },
|
||||
{ key: "serverState", method: "GET", path: "/v3/admin/core/state", query: "", body: "" },
|
||||
{ key: "namespaceList", method: "GET", path: "/v3/admin/core/namespace/list", query: "", body: "" },
|
||||
{
|
||||
key: "configDetail",
|
||||
method: "GET",
|
||||
path: "/v3/console/cs/config",
|
||||
path: "/v3/admin/cs/config",
|
||||
query: "dataId=application.yaml&groupName=DEFAULT_GROUP&namespaceId=",
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
key: "serviceList",
|
||||
method: "GET",
|
||||
path: "/v3/console/ns/service/list",
|
||||
path: "/v3/admin/ns/service/list",
|
||||
query: "pageNo=1&pageSize=20&namespaceId=",
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
key: "instanceList",
|
||||
method: "GET",
|
||||
path: "/v3/console/ns/instance/list",
|
||||
path: "/v3/admin/ns/instance/list",
|
||||
query: "serviceName=DEFAULT_GROUP@@example&namespaceId=",
|
||||
body: "",
|
||||
},
|
||||
|
|
@ -702,8 +702,12 @@ export function buildNacosInstanceConfirm(service: NacosServiceInfo, instance: N
|
|||
`serviceName=${service.serviceName}`,
|
||||
`group=${instance.groupName || service.groupName || fallbackGroup || "DEFAULT_GROUP"}`,
|
||||
`instance=${instance.ip}:${instance.port}`,
|
||||
`cluster=${instance.clusterName || "DEFAULT"}`,
|
||||
`ephemeral=${instance.ephemeral === true ? "true" : instance.ephemeral === false ? "false" : "unknown"}`,
|
||||
patch.enabled == null ? "" : `targetEnabled=${targetEnabled === false ? "false" : "true"}`,
|
||||
patch.healthy == null ? "" : `targetHealthy=${targetHealthy === false ? "false" : "true"}`,
|
||||
patch.weight == null ? "" : `targetWeight=${patch.weight}`,
|
||||
patch.metadata == null ? "" : `targetMetadata=${JSON.stringify(patch.metadata)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import type { NacosNamespaceInfo } from "@/types/nacos";
|
||||
|
||||
/**
|
||||
* Nacos 2/r-nacos may represent the default namespace with an empty ID, while
|
||||
* Nacos 3 returns the concrete `public` ID. Older backend builds could expose
|
||||
* both forms at once; prefer the concrete ID and keep every other namespace.
|
||||
*/
|
||||
export function normalizeNacosNamespacesForDisplay(namespaces: NacosNamespaceInfo[]): NacosNamespaceInfo[] {
|
||||
const normalized = new Map<string, NacosNamespaceInfo>();
|
||||
for (const namespace of namespaces) {
|
||||
const identity = nacosNamespaceIdentity(namespace.namespace);
|
||||
const existing = normalized.get(identity);
|
||||
// Prefer the concrete public ID when both legacy representations are returned.
|
||||
if (!existing || (identity === "public" && namespace.namespace === "public" && existing.namespace === "")) {
|
||||
normalized.set(identity, namespace);
|
||||
}
|
||||
}
|
||||
return [...normalized.values()];
|
||||
}
|
||||
|
||||
/** Returns the stable display/filter identity without changing the value sent to Nacos. */
|
||||
export function nacosNamespaceIdentity(namespace: string): string {
|
||||
return namespace === "" || namespace === "public" ? "public" : namespace;
|
||||
}
|
||||
|
||||
/** Converts selected identities back to the namespace IDs returned by this Nacos endpoint. */
|
||||
export function normalizeNacosNamespaceSelection(selected: Iterable<string>, namespaces: NacosNamespaceInfo[]): string[] {
|
||||
const available = new Map<string, string>();
|
||||
for (const namespace of normalizeNacosNamespacesForDisplay(namespaces)) {
|
||||
available.set(nacosNamespaceIdentity(namespace.namespace), namespace.namespace);
|
||||
}
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of selected) {
|
||||
const identity = nacosNamespaceIdentity(value);
|
||||
const namespace = available.get(identity);
|
||||
if (namespace !== undefined && !seen.has(identity)) {
|
||||
seen.add(identity);
|
||||
result.push(namespace);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `visible_databases` stores the namespace identifiers selected for a Nacos connection. */
|
||||
export function filterNacosNamespacesForSidebar(namespaces: NacosNamespaceInfo[], visibleNamespaces: string[] | undefined): NacosNamespaceInfo[] {
|
||||
const normalized = normalizeNacosNamespacesForDisplay(namespaces);
|
||||
if (!Array.isArray(visibleNamespaces)) return normalized;
|
||||
const visible = new Set(visibleNamespaces.map(nacosNamespaceIdentity));
|
||||
return normalized.filter((namespace) => visible.has(nacosNamespaceIdentity(namespace.namespace)));
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import type { NacosInstanceInfo, NacosInstancePatch, NacosInstanceRef, NacosServiceDetail, NacosServiceUpsert } from "@/types/nacos";
|
||||
|
||||
function canonicalJson(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(canonicalJson);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, nested]) => [key, canonicalJson(nested)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function nacosJsonObjectMatches(left: unknown, right: unknown) {
|
||||
return JSON.stringify(canonicalJson(left ?? {})) === JSON.stringify(canonicalJson(right ?? {}));
|
||||
}
|
||||
|
||||
export function nacosIpAddressIsValid(value: string) {
|
||||
const input = value.trim();
|
||||
if (!input) return false;
|
||||
if (input.includes(":")) {
|
||||
try {
|
||||
const parsed = new URL(`http://[${input}]/`);
|
||||
return parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const octets = input.split(".");
|
||||
return octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && (octet === "0" || !octet.startsWith("0")) && Number(octet) <= 255);
|
||||
}
|
||||
|
||||
export function nacosInstanceRefIdentity(ref: NacosInstanceRef) {
|
||||
const lifetime = ref.ephemeral === true ? "ephemeral" : ref.ephemeral === false ? "persistent" : "unknown";
|
||||
return [ref.namespace || "public", ref.groupName || "DEFAULT_GROUP", ref.serviceName, ref.ip, ref.port, ref.clusterName || "DEFAULT", lifetime].join("\u0000");
|
||||
}
|
||||
|
||||
export function nacosInstanceMatchesPatch(instance: NacosInstanceInfo, patch: NacosInstancePatch) {
|
||||
const weightMatches = patch.weight == null || (instance.weight != null && Math.abs(instance.weight - patch.weight) <= 1e-6);
|
||||
const metadataMatches = patch.metadata == null || nacosJsonObjectMatches(instance.metadata, patch.metadata);
|
||||
return (patch.enabled == null || instance.enabled === patch.enabled) && (patch.healthy == null || instance.healthy === patch.healthy) && weightMatches && metadataMatches;
|
||||
}
|
||||
|
||||
export function normalizeNacosSelector(value: unknown): unknown {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length === 0) return null;
|
||||
const selector = value as Record<string, unknown>;
|
||||
const type = String(selector.type ?? "").toLowerCase();
|
||||
const contextType = String(selector.contextType ?? "").toUpperCase();
|
||||
if (type === "none" || type === "noneselector" || contextType === "NONE") return null;
|
||||
return canonicalJson(selector);
|
||||
}
|
||||
|
||||
export function nacosServiceDetailMatches(detail: NacosServiceDetail, expected: NacosServiceUpsert) {
|
||||
const thresholdMatches = expected.protectThreshold == null || (detail.protectThreshold != null && Math.abs(detail.protectThreshold - expected.protectThreshold) <= 1e-6);
|
||||
return nacosJsonObjectMatches(detail.metadata, expected.metadata) && thresholdMatches && JSON.stringify(normalizeNacosSelector(detail.selector)) === JSON.stringify(normalizeNacosSelector(expected.selector));
|
||||
}
|
||||
|
|
@ -109,6 +109,7 @@ import { normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel }
|
|||
import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVersion, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint";
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
|
||||
import { filterNacosNamespacesForSidebar } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
import { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
|
||||
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
|
||||
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
|
||||
|
|
@ -3438,7 +3439,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
if (useCachedChildren(node, options, load)) return;
|
||||
|
||||
const namespaces = await api.nacosListNamespaces(connectionId);
|
||||
const sorted = [...namespaces].sort((left, right) => {
|
||||
const visibleNamespaces = filterNacosNamespacesForSidebar(namespaces, getConfig(connectionId)?.visible_databases);
|
||||
const sorted = [...visibleNamespaces].sort((left, right) => {
|
||||
const leftLabel = left.namespaceShowName || left.namespace || "public";
|
||||
const rightLabel = right.namespaceShowName || right.namespace || "public";
|
||||
return leftLabel.localeCompare(rightLabel);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,28 @@ export interface NacosCapabilities {
|
|||
supportsServiceManagement: boolean;
|
||||
supportsInstanceUpdate: boolean;
|
||||
supportsRawApi: boolean;
|
||||
/** Naming capabilities are intentionally granular: servers such as r-nacos
|
||||
* can expose discovery reads without supporting the official write APIs. */
|
||||
serviceManagement?: NacosServiceCapabilities;
|
||||
}
|
||||
|
||||
export interface NacosServiceCapabilities {
|
||||
listServices: NacosOperationCapability;
|
||||
getService: NacosOperationCapability;
|
||||
createService: NacosOperationCapability;
|
||||
updateService: NacosOperationCapability;
|
||||
deleteService: NacosOperationCapability;
|
||||
listInstances: NacosOperationCapability;
|
||||
updateInstance: NacosOperationCapability;
|
||||
registerInstance: NacosOperationCapability;
|
||||
deregisterInstance: NacosOperationCapability;
|
||||
}
|
||||
|
||||
export type NacosCapabilityReason = "implementationReadOnly" | "versionUnsupported" | "endpointUnavailable" | "notVerified" | "connectionReadOnly";
|
||||
|
||||
export interface NacosOperationCapability {
|
||||
supported: boolean;
|
||||
reason?: NacosCapabilityReason;
|
||||
}
|
||||
|
||||
export interface NacosConnectionInfo {
|
||||
|
|
@ -58,7 +80,6 @@ export interface NacosAdminConfig {
|
|||
implementation?: NacosImplementation;
|
||||
versionMode?: NacosVersionMode;
|
||||
serverAddr: string;
|
||||
namespace?: string;
|
||||
contextPath?: string;
|
||||
rnacosConsoleAddr?: string;
|
||||
/** Undefined keeps the legacy behaviour: history is enabled when a console address exists. */
|
||||
|
|
@ -313,6 +334,25 @@ export interface NacosServiceList {
|
|||
items: NacosServiceInfo[];
|
||||
}
|
||||
|
||||
export interface NacosServiceDetail {
|
||||
serviceName: string;
|
||||
groupName?: string;
|
||||
metadata?: unknown;
|
||||
protectThreshold?: number;
|
||||
selector?: unknown;
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
|
||||
export interface NacosServiceUpsert {
|
||||
namespace?: string;
|
||||
serviceName: string;
|
||||
groupName?: string;
|
||||
metadata?: unknown;
|
||||
protectThreshold?: number;
|
||||
selector?: unknown;
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
|
||||
export interface NacosInstanceQuery {
|
||||
namespace?: string;
|
||||
serviceName: string;
|
||||
|
|
@ -333,18 +373,37 @@ export interface NacosInstanceInfo {
|
|||
metadata?: unknown;
|
||||
}
|
||||
|
||||
export interface NacosInstanceUpdate {
|
||||
export interface NacosInstanceRef {
|
||||
namespace?: string;
|
||||
serviceName: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
groupName?: string;
|
||||
clusterName?: string;
|
||||
ephemeral?: boolean;
|
||||
}
|
||||
|
||||
export interface NacosInstancePatch {
|
||||
healthy?: boolean;
|
||||
enabled?: boolean;
|
||||
ephemeral?: boolean;
|
||||
weight?: number;
|
||||
metadata?: unknown;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface NacosInstanceUpdateRequest {
|
||||
target: NacosInstanceRef;
|
||||
patch: NacosInstancePatch;
|
||||
}
|
||||
|
||||
export interface NacosInstanceRegistration {
|
||||
namespace?: string;
|
||||
serviceName: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
groupName?: string;
|
||||
clusterName?: string;
|
||||
weight?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface NacosDashboardQuery {
|
||||
|
|
|
|||
|
|
@ -618,11 +618,31 @@ mod tests {
|
|||
Err("unused".to_string())
|
||||
}
|
||||
|
||||
async fn get_service(&self, _: NacosServiceQuery) -> Result<NacosServiceDetail, String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn create_service(&self, _: NacosServiceUpsert) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn update_service(&self, _: NacosServiceUpsert) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn delete_service(&self, _: NacosServiceQuery) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
|
||||
async fn list_instances(&self, _: NacosInstanceQuery) -> Result<Vec<NacosInstanceInfo>, String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
|
||||
async fn update_instance(&self, _: NacosInstanceUpdate) -> Result<(), String> {
|
||||
async fn update_instance(&self, _: NacosInstanceUpdateRequest) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
|
||||
async fn register_instance(&self, _: NacosInstanceRegistration) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn deregister_instance(&self, _: NacosInstanceRef) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,10 +147,9 @@ impl NacosAdminConfig {
|
|||
}
|
||||
let context_path_is_explicit_root = self.context_path.trim() == "/";
|
||||
self.context_path = normalize_context_path(&self.context_path);
|
||||
// Nacos 3 separates the console (normally :8080) from the server-side
|
||||
// Admin API (normally :8848/nacos). Older DBX connection records did
|
||||
// not persist the default server context, so repair only explicit
|
||||
// Nacos 3 profiles here while preserving custom contexts.
|
||||
// Nacos 3 management uses the server-side Admin API, normally
|
||||
// `:8848/nacos`. Keep the documented default context for explicit V3
|
||||
// profiles while preserving custom reverse-proxy prefixes.
|
||||
if self.context_path.is_empty()
|
||||
&& !context_path_is_explicit_root
|
||||
&& matches!(self.implementation, Some(NacosImplementation::Nacos))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,10 @@ use crate::nacos::types::*;
|
|||
|
||||
#[async_trait]
|
||||
pub trait NacosAdmin: Send + Sync {
|
||||
fn service_capabilities(&self) -> NacosServiceCapabilities {
|
||||
NacosServiceCapabilities::default()
|
||||
}
|
||||
|
||||
async fn test_connection(&self) -> Result<NacosConnectionInfo, String>;
|
||||
async fn list_namespaces(&self) -> Result<Vec<NacosNamespaceInfo>, String>;
|
||||
async fn create_namespace(&self, req: NacosNamespaceCreate) -> Result<(), String>;
|
||||
|
|
@ -29,8 +33,23 @@ pub trait NacosAdmin: Send + Sync {
|
|||
async fn get_rnacos_console_captcha(&self) -> Result<NacosRNacosConsoleCaptcha, String>;
|
||||
async fn login_rnacos_console(&self, captcha: Option<String>) -> Result<(), String>;
|
||||
async fn list_services(&self, query: NacosServiceQuery) -> Result<NacosServiceList, String>;
|
||||
async fn get_service(&self, query: NacosServiceQuery) -> Result<NacosServiceDetail, String>;
|
||||
async fn create_service(&self, req: NacosServiceUpsert) -> Result<(), String>;
|
||||
async fn update_service(&self, req: NacosServiceUpsert) -> Result<(), String>;
|
||||
async fn delete_service(&self, query: NacosServiceQuery) -> Result<(), String>;
|
||||
async fn list_instances(&self, query: NacosInstanceQuery) -> Result<Vec<NacosInstanceInfo>, String>;
|
||||
async fn update_instance(&self, req: NacosInstanceUpdate) -> Result<(), String>;
|
||||
/// Returns the authoritative management view used before deleting a
|
||||
/// service. Implementations whose discovery API hides disabled instances
|
||||
/// must override this instead of falling back to that lossy view.
|
||||
async fn list_instances_for_service_delete(
|
||||
&self,
|
||||
query: NacosInstanceQuery,
|
||||
) -> Result<Vec<NacosInstanceInfo>, String> {
|
||||
self.list_instances(query).await
|
||||
}
|
||||
async fn update_instance(&self, req: NacosInstanceUpdateRequest) -> Result<(), String>;
|
||||
async fn register_instance(&self, req: NacosInstanceRegistration) -> Result<(), String>;
|
||||
async fn deregister_instance(&self, req: NacosInstanceRef) -> Result<(), String>;
|
||||
async fn get_dashboard(&self, query: NacosDashboardQuery) -> Result<NacosDashboardSnapshot, String>;
|
||||
async fn raw_request(&self, req: NacosRawRequest) -> Result<NacosRawResponse, String>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -693,10 +693,28 @@ mod tests {
|
|||
async fn list_services(&self, _: NacosServiceQuery) -> Result<NacosServiceList, String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn get_service(&self, _: NacosServiceQuery) -> Result<NacosServiceDetail, String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn create_service(&self, _: NacosServiceUpsert) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn update_service(&self, _: NacosServiceUpsert) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn delete_service(&self, _: NacosServiceQuery) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn list_instances(&self, _: NacosInstanceQuery) -> Result<Vec<NacosInstanceInfo>, String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn update_instance(&self, _: NacosInstanceUpdate) -> Result<(), String> {
|
||||
async fn update_instance(&self, _: NacosInstanceUpdateRequest) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn register_instance(&self, _: NacosInstanceRegistration) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn deregister_instance(&self, _: NacosInstanceRef) -> Result<(), String> {
|
||||
Err("unused".to_string())
|
||||
}
|
||||
async fn get_dashboard(&self, _: NacosDashboardQuery) -> Result<NacosDashboardSnapshot, String> {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ pub async fn nacos_list_services_core(
|
|||
query: NacosServiceQuery,
|
||||
) -> Result<NacosServiceList, String> {
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::ListServices)?;
|
||||
admin.list_services(query).await
|
||||
}
|
||||
|
||||
|
|
@ -149,19 +150,100 @@ pub async fn nacos_list_instances_core(
|
|||
query: NacosInstanceQuery,
|
||||
) -> Result<Vec<NacosInstanceInfo>, String> {
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::ListInstances)?;
|
||||
admin.list_instances(query).await
|
||||
}
|
||||
|
||||
pub async fn nacos_get_service_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
query: NacosServiceQuery,
|
||||
) -> Result<NacosServiceDetail, String> {
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::GetService)?;
|
||||
admin.get_service(query).await
|
||||
}
|
||||
|
||||
pub async fn nacos_create_service_core(state: &AppState, conn_id: &str, req: NacosServiceUpsert) -> Result<(), String> {
|
||||
ensure_connection_writable(state, conn_id, "Create Nacos service").await?;
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::CreateService)?;
|
||||
admin.create_service(req).await
|
||||
}
|
||||
|
||||
pub async fn nacos_update_service_core(state: &AppState, conn_id: &str, req: NacosServiceUpsert) -> Result<(), String> {
|
||||
ensure_connection_writable(state, conn_id, "Update Nacos service").await?;
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::UpdateService)?;
|
||||
admin.update_service(req).await
|
||||
}
|
||||
|
||||
pub async fn nacos_delete_service_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
query: NacosServiceQuery,
|
||||
) -> Result<(), String> {
|
||||
ensure_connection_writable(state, conn_id, "Delete Nacos service").await?;
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::DeleteService)?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::ListInstances)?;
|
||||
let service_name = query
|
||||
.service_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.ok_or_else(|| "Nacos service name is required".to_string())?
|
||||
.to_string();
|
||||
let instances = admin
|
||||
.list_instances_for_service_delete(NacosInstanceQuery {
|
||||
namespace: query.namespace.clone(),
|
||||
service_name,
|
||||
group_name: query.group_name.clone(),
|
||||
clusters: None,
|
||||
})
|
||||
.await?;
|
||||
if !instances.is_empty() {
|
||||
return Err(format!(
|
||||
"NACOS_ERROR[serviceNotEmpty]: Nacos service still contains {} instance(s); deregister them before deletion",
|
||||
instances.len()
|
||||
));
|
||||
}
|
||||
admin.delete_service(query).await
|
||||
}
|
||||
|
||||
pub async fn nacos_update_instance_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
req: NacosInstanceUpdate,
|
||||
req: NacosInstanceUpdateRequest,
|
||||
) -> Result<(), String> {
|
||||
ensure_connection_writable(state, conn_id, "Update Nacos instance").await?;
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::UpdateInstance)?;
|
||||
admin.update_instance(req).await
|
||||
}
|
||||
|
||||
pub async fn nacos_register_instance_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
req: NacosInstanceRegistration,
|
||||
) -> Result<(), String> {
|
||||
ensure_connection_writable(state, conn_id, "Register Nacos instance").await?;
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::RegisterInstance)?;
|
||||
admin.register_instance(req).await
|
||||
}
|
||||
|
||||
pub async fn nacos_deregister_instance_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
req: NacosInstanceRef,
|
||||
) -> Result<(), String> {
|
||||
ensure_connection_writable(state, conn_id, "Deregister Nacos instance").await?;
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
ensure_service_operation(admin.as_ref(), NacosServiceOperation::DeregisterInstance)?;
|
||||
admin.deregister_instance(req).await
|
||||
}
|
||||
|
||||
pub async fn nacos_get_dashboard_core(
|
||||
state: &AppState,
|
||||
conn_id: &str,
|
||||
|
|
@ -205,10 +287,65 @@ pub(crate) async fn ensure_connection_writable(state: &AppState, conn_id: &str,
|
|||
}
|
||||
}
|
||||
|
||||
fn ensure_service_operation(
|
||||
admin: &dyn crate::nacos::port::NacosAdmin,
|
||||
operation: NacosServiceOperation,
|
||||
) -> Result<(), String> {
|
||||
let capabilities = admin.service_capabilities();
|
||||
let capability = capabilities.operation(operation);
|
||||
if capability.supported {
|
||||
return Ok(());
|
||||
}
|
||||
let reason = match capability.reason {
|
||||
Some(NacosCapabilityReason::ImplementationReadOnly) => "implementationReadOnly",
|
||||
Some(NacosCapabilityReason::VersionUnsupported) => "versionUnsupported",
|
||||
Some(NacosCapabilityReason::EndpointUnavailable) => "endpointUnavailable",
|
||||
Some(NacosCapabilityReason::NotVerified) => "notVerified",
|
||||
Some(NacosCapabilityReason::ConnectionReadOnly) => "connectionReadOnly",
|
||||
None => "notVerified",
|
||||
};
|
||||
Err(format!("NACOS_ERROR[unsupportedOperation]: Nacos service operation {operation:?} is unavailable ({reason})"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn core_service_operation_guard_allows_documented_rnacos_v1_writes() {
|
||||
use crate::nacos::config::{
|
||||
NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosRNacosConsoleAuth,
|
||||
NacosVersionMode,
|
||||
};
|
||||
use crate::nacos::http::NacosOpenApiAdmin;
|
||||
use crate::nacos::port::NacosAdmin;
|
||||
|
||||
let admin = NacosOpenApiAdmin::new(NacosAdminConfig {
|
||||
implementation: Some(NacosImplementation::RNacos),
|
||||
server_addr: "http://127.0.0.1:3848".to_string(),
|
||||
display_server_addr: "http://127.0.0.1:3848".to_string(),
|
||||
namespace: "public".to_string(),
|
||||
version_mode: Some(NacosVersionMode::Auto),
|
||||
context_path: "/nacos".to_string(),
|
||||
rnacos_console_addr: String::new(),
|
||||
rnacos_history_enabled: Some(false),
|
||||
rnacos_console_auth: NacosRNacosConsoleAuth::Inherit,
|
||||
auth: NacosAuthConfig::None,
|
||||
tls_skip_verify: false,
|
||||
metrics_mode: NacosMetricsMode::Disabled,
|
||||
metrics_url: String::new(),
|
||||
page_size: 20,
|
||||
connect_override: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(ensure_service_operation(&admin, NacosServiceOperation::ListServices).is_ok());
|
||||
assert!(ensure_service_operation(&admin, NacosServiceOperation::CreateService).is_ok());
|
||||
assert!(ensure_service_operation(&admin, NacosServiceOperation::UpdateInstance).is_ok());
|
||||
assert!(admin.service_capabilities().create_service.supported);
|
||||
let error = ensure_service_operation(&admin, NacosServiceOperation::DeleteService).unwrap_err();
|
||||
assert!(error.contains("endpointUnavailable"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raw_mutation_requires_writable_connection_before_adapter_build() {
|
||||
let dir = std::env::temp_dir().join(format!("dbx-nacos-service-test-{}", uuid::Uuid::new_v4()));
|
||||
|
|
|
|||
|
|
@ -10,6 +10,99 @@ pub struct NacosCapabilities {
|
|||
pub supports_service_management: bool,
|
||||
pub supports_instance_update: bool,
|
||||
pub supports_raw_api: bool,
|
||||
#[serde(default)]
|
||||
pub service_management: NacosServiceCapabilities,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosOperationCapability {
|
||||
pub supported: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<NacosCapabilityReason>,
|
||||
}
|
||||
|
||||
impl NacosOperationCapability {
|
||||
pub fn supported() -> Self {
|
||||
Self { supported: true, reason: None }
|
||||
}
|
||||
|
||||
pub fn unsupported(reason: NacosCapabilityReason) -> Self {
|
||||
Self { supported: false, reason: Some(reason) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NacosOperationCapability {
|
||||
fn default() -> Self {
|
||||
Self::supported()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum NacosCapabilityReason {
|
||||
ImplementationReadOnly,
|
||||
VersionUnsupported,
|
||||
EndpointUnavailable,
|
||||
NotVerified,
|
||||
ConnectionReadOnly,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NacosServiceOperation {
|
||||
ListServices,
|
||||
GetService,
|
||||
CreateService,
|
||||
UpdateService,
|
||||
DeleteService,
|
||||
ListInstances,
|
||||
UpdateInstance,
|
||||
RegisterInstance,
|
||||
DeregisterInstance,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosServiceCapabilities {
|
||||
pub list_services: NacosOperationCapability,
|
||||
pub get_service: NacosOperationCapability,
|
||||
pub create_service: NacosOperationCapability,
|
||||
pub update_service: NacosOperationCapability,
|
||||
pub delete_service: NacosOperationCapability,
|
||||
pub list_instances: NacosOperationCapability,
|
||||
pub update_instance: NacosOperationCapability,
|
||||
pub register_instance: NacosOperationCapability,
|
||||
pub deregister_instance: NacosOperationCapability,
|
||||
}
|
||||
|
||||
impl NacosServiceCapabilities {
|
||||
pub fn read_only(reason: NacosCapabilityReason) -> Self {
|
||||
Self {
|
||||
list_services: NacosOperationCapability::supported(),
|
||||
get_service: NacosOperationCapability::supported(),
|
||||
create_service: NacosOperationCapability::unsupported(reason),
|
||||
update_service: NacosOperationCapability::unsupported(reason),
|
||||
delete_service: NacosOperationCapability::unsupported(reason),
|
||||
list_instances: NacosOperationCapability::supported(),
|
||||
update_instance: NacosOperationCapability::unsupported(reason),
|
||||
register_instance: NacosOperationCapability::unsupported(reason),
|
||||
deregister_instance: NacosOperationCapability::unsupported(reason),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&self, operation: NacosServiceOperation) -> &NacosOperationCapability {
|
||||
match operation {
|
||||
NacosServiceOperation::ListServices => &self.list_services,
|
||||
NacosServiceOperation::GetService => &self.get_service,
|
||||
NacosServiceOperation::CreateService => &self.create_service,
|
||||
NacosServiceOperation::UpdateService => &self.update_service,
|
||||
NacosServiceOperation::DeleteService => &self.delete_service,
|
||||
NacosServiceOperation::ListInstances => &self.list_instances,
|
||||
NacosServiceOperation::UpdateInstance => &self.update_instance,
|
||||
NacosServiceOperation::RegisterInstance => &self.register_instance,
|
||||
NacosServiceOperation::DeregisterInstance => &self.deregister_instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NacosCapabilities {
|
||||
|
|
@ -21,6 +114,7 @@ impl Default for NacosCapabilities {
|
|||
supports_service_management: true,
|
||||
supports_instance_update: true,
|
||||
supports_raw_api: true,
|
||||
service_management: NacosServiceCapabilities::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -437,6 +531,40 @@ pub struct NacosServiceList {
|
|||
pub items: Vec<NacosServiceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosServiceDetail {
|
||||
pub service_name: String,
|
||||
#[serde(default)]
|
||||
pub group_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub metadata: serde_json::Value,
|
||||
#[serde(default)]
|
||||
pub protect_threshold: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub selector: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub ephemeral: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosServiceUpsert {
|
||||
#[serde(default)]
|
||||
pub namespace: Option<String>,
|
||||
pub service_name: String,
|
||||
#[serde(default)]
|
||||
pub group_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub protect_threshold: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub selector: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub ephemeral: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosInstanceInfo {
|
||||
|
|
@ -474,7 +602,7 @@ pub struct NacosInstanceQuery {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosInstanceUpdate {
|
||||
pub struct NacosInstanceRef {
|
||||
#[serde(default)]
|
||||
pub namespace: Option<String>,
|
||||
pub service_name: String,
|
||||
|
|
@ -484,12 +612,42 @@ pub struct NacosInstanceUpdate {
|
|||
pub group_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cluster_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ephemeral: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosInstancePatch {
|
||||
#[serde(default)]
|
||||
pub healthy: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub ephemeral: Option<bool>,
|
||||
pub weight: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosInstanceUpdateRequest {
|
||||
pub target: NacosInstanceRef,
|
||||
pub patch: NacosInstancePatch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosInstanceRegistration {
|
||||
#[serde(default)]
|
||||
pub namespace: Option<String>,
|
||||
pub service_name: String,
|
||||
pub ip: String,
|
||||
pub port: u16,
|
||||
#[serde(default)]
|
||||
pub group_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cluster_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub weight: Option<f64>,
|
||||
#[serde(default)]
|
||||
|
|
@ -503,6 +661,86 @@ pub struct NacosDashboardQuery {
|
|||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod service_capability_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn official_nacos_service_capabilities_enable_every_supported_operation() {
|
||||
let capabilities = NacosServiceCapabilities::default();
|
||||
for operation in [
|
||||
NacosServiceOperation::ListServices,
|
||||
NacosServiceOperation::GetService,
|
||||
NacosServiceOperation::CreateService,
|
||||
NacosServiceOperation::UpdateService,
|
||||
NacosServiceOperation::DeleteService,
|
||||
NacosServiceOperation::ListInstances,
|
||||
NacosServiceOperation::UpdateInstance,
|
||||
NacosServiceOperation::RegisterInstance,
|
||||
NacosServiceOperation::DeregisterInstance,
|
||||
] {
|
||||
assert!(capabilities.operation(operation).supported);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_implementation_preserves_reads_and_explains_writes() {
|
||||
let capabilities = NacosServiceCapabilities::read_only(NacosCapabilityReason::ImplementationReadOnly);
|
||||
assert!(capabilities.list_services.supported);
|
||||
assert!(capabilities.get_service.supported);
|
||||
assert!(capabilities.list_instances.supported);
|
||||
assert_eq!(capabilities.update_instance.reason, Some(NacosCapabilityReason::ImplementationReadOnly));
|
||||
assert_eq!(capabilities.create_service.reason, Some(NacosCapabilityReason::ImplementationReadOnly));
|
||||
assert_eq!(capabilities.deregister_instance.reason, Some(NacosCapabilityReason::ImplementationReadOnly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_capabilities_use_the_tauri_and_web_camel_case_contract() {
|
||||
let value =
|
||||
serde_json::to_value(NacosServiceCapabilities::read_only(NacosCapabilityReason::NotVerified)).unwrap();
|
||||
assert_eq!(value["listServices"]["supported"], true);
|
||||
assert_eq!(value["createService"]["supported"], false);
|
||||
assert_eq!(value["createService"]["reason"], "notVerified");
|
||||
assert!(value.get("manageServices").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_update_uses_nested_target_and_patch_transport_contract() {
|
||||
let value = serde_json::to_value(NacosInstanceUpdateRequest {
|
||||
target: NacosInstanceRef {
|
||||
namespace: Some("public".to_string()),
|
||||
service_name: "api".to_string(),
|
||||
ip: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
group_name: Some("DBX_TEST".to_string()),
|
||||
cluster_name: Some("blue".to_string()),
|
||||
ephemeral: Some(false),
|
||||
},
|
||||
patch: NacosInstancePatch { enabled: Some(false), ..Default::default() },
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(value["target"]["serviceName"], "api");
|
||||
assert_eq!(value["target"]["ephemeral"], false);
|
||||
assert_eq!(value["patch"]["enabled"], false);
|
||||
assert!(value.get("serviceName").is_none());
|
||||
assert!(value["patch"].get("weight").is_some_and(serde_json::Value::is_null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_capabilities_without_the_operation_matrix_remain_readable() {
|
||||
let capabilities: NacosCapabilities = serde_json::from_value(serde_json::json!({
|
||||
"supportsConfigManagement": true,
|
||||
"supportsConfigHistory": true,
|
||||
"supportsServiceManagement": true,
|
||||
"supportsInstanceUpdate": true,
|
||||
"supportsRawApi": true
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(capabilities.service_management.list_services.supported);
|
||||
assert!(capabilities.service_management.update_instance.supported);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NacosDashboardMetrics {
|
||||
|
|
|
|||
|
|
@ -323,11 +323,8 @@ impl LocalBackend {
|
|||
}
|
||||
}
|
||||
}
|
||||
let stale_ids: Vec<String> = runtime
|
||||
.keys()
|
||||
.filter(|id| !configs.iter().any(|config| &config.id == *id))
|
||||
.cloned()
|
||||
.collect();
|
||||
let stale_ids: Vec<String> =
|
||||
runtime.keys().filter(|id| !configs.iter().any(|config| &config.id == *id)).cloned().collect();
|
||||
for id in stale_ids {
|
||||
runtime.remove(&id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{ffi::OsString, sync::Arc};
|
||||
|
||||
use dbx_core::{models::connection::ConnectionConfig, storage::Storage};
|
||||
use dbx_mcp::{DbxMcpServer, LocalBackend, McpScope};
|
||||
use dbx_mcp::{DbxBackend, DbxMcpServer, LocalBackend, McpScope};
|
||||
use rmcp::{model::CallToolRequestParams, ServiceExt};
|
||||
use serde_json::{json, Map, Value};
|
||||
use tempfile::tempdir;
|
||||
|
|
@ -111,11 +111,8 @@ async fn local_backend_picks_up_connections_added_after_startup_without_reload()
|
|||
storage.save_connections(&[initial, added.clone()]).await.expect("save added connection");
|
||||
|
||||
// list_connections reads storage live, so the new connection is already visible.
|
||||
let list_result = client
|
||||
.peer()
|
||||
.call_tool(CallToolRequestParams::new("dbx_list_connections"))
|
||||
.await
|
||||
.expect("list connections");
|
||||
let list_result =
|
||||
client.peer().call_tool(CallToolRequestParams::new("dbx_list_connections")).await.expect("list connections");
|
||||
let list_text = list_result.content[0].as_text().expect("text response").text.clone();
|
||||
assert!(list_text.contains("added-sqlite"), "list should include added connection: {list_text}");
|
||||
|
||||
|
|
|
|||
|
|
@ -573,8 +573,14 @@ async fn main() {
|
|||
.route("/nacos/rnacos-console/captcha", post(routes::nacos::get_rnacos_console_captcha))
|
||||
.route("/nacos/rnacos-console/login", post(routes::nacos::login_rnacos_console))
|
||||
.route("/nacos/services/list", post(routes::nacos::list_services))
|
||||
.route("/nacos/services/get", post(routes::nacos::get_service))
|
||||
.route("/nacos/services/create", post(routes::nacos::create_service))
|
||||
.route("/nacos/services/update", post(routes::nacos::update_service))
|
||||
.route("/nacos/services/delete", post(routes::nacos::delete_service))
|
||||
.route("/nacos/instances/list", post(routes::nacos::list_instances))
|
||||
.route("/nacos/instances/update", post(routes::nacos::update_instance))
|
||||
.route("/nacos/instances/register", post(routes::nacos::register_instance))
|
||||
.route("/nacos/instances/deregister", post(routes::nacos::deregister_instance))
|
||||
.route("/nacos/dashboard", post(routes::nacos::get_dashboard))
|
||||
.route("/nacos/raw", post(routes::nacos::raw_request))
|
||||
.route("/nacos/configs/search", post(routes::nacos::search_config_content))
|
||||
|
|
|
|||
|
|
@ -95,6 +95,20 @@ pub(crate) struct ServiceListReq {
|
|||
query: dbx_core::nacos::NacosServiceQuery,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ServiceQueryReq {
|
||||
connection_id: String,
|
||||
query: dbx_core::nacos::NacosServiceQuery,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ServiceUpsertReq {
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosServiceUpsert,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct InstanceListReq {
|
||||
|
|
@ -106,7 +120,21 @@ pub(crate) struct InstanceListReq {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct InstanceUpdateReq {
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosInstanceUpdate,
|
||||
req: dbx_core::nacos::NacosInstanceUpdateRequest,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct InstanceRegistrationReq {
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosInstanceRegistration,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct InstanceRefReq {
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosInstanceRef,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
|
|
@ -319,6 +347,46 @@ pub async fn list_instances(
|
|||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn get_service(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ServiceQueryReq>,
|
||||
) -> Result<Json<dbx_core::nacos::NacosServiceDetail>, AppError> {
|
||||
let result = dbx_core::nacos::service::nacos_get_service_core(&state.app, &req.connection_id, req.query)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn create_service(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ServiceUpsertReq>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
dbx_core::nacos::service::nacos_create_service_core(&state.app, &req.connection_id, req.req)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn update_service(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ServiceUpsertReq>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
dbx_core::nacos::service::nacos_update_service_core(&state.app, &req.connection_id, req.req)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn delete_service(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ServiceQueryReq>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
dbx_core::nacos::service::nacos_delete_service_core(&state.app, &req.connection_id, req.query)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn update_instance(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<InstanceUpdateReq>,
|
||||
|
|
@ -329,6 +397,26 @@ pub async fn update_instance(
|
|||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn register_instance(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<InstanceRegistrationReq>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
dbx_core::nacos::service::nacos_register_instance_core(&state.app, &req.connection_id, req.req)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn deregister_instance(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<InstanceRefReq>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
dbx_core::nacos::service::nacos_deregister_instance_core(&state.app, &req.connection_id, req.req)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn get_dashboard(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<DashboardReq>,
|
||||
|
|
@ -754,4 +842,31 @@ mod batch_tests {
|
|||
request.connection_id = "connection-b".to_string();
|
||||
assert!(validate_nacos_import_context(&context, Some("preview-session"), &request).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_update_route_uses_target_and_patch_contract() {
|
||||
let request: InstanceUpdateReq = serde_json::from_value(serde_json::json!({
|
||||
"connectionId": "nacos-v2",
|
||||
"req": {
|
||||
"target": {
|
||||
"namespace": "public",
|
||||
"groupName": "DBX_TEST",
|
||||
"serviceName": "api",
|
||||
"ip": "127.0.0.1",
|
||||
"port": 8080,
|
||||
"clusterName": "blue",
|
||||
"ephemeral": false
|
||||
},
|
||||
"patch": {
|
||||
"weight": 2.5,
|
||||
"metadata": { "role": "api" }
|
||||
}
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(request.connection_id, "nacos-v2");
|
||||
assert_eq!(request.req.target.ephemeral, Some(false));
|
||||
assert_eq!(request.req.patch.weight, Some(2.5));
|
||||
assert_eq!(request.req.patch.metadata, Some(serde_json::json!({ "role": "api" })));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,42 @@ pub async fn nacos_list_services(
|
|||
dbx_core::nacos::service::nacos_list_services_core(&state, &connection_id, query).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_get_service(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
query: dbx_core::nacos::NacosServiceQuery,
|
||||
) -> Result<dbx_core::nacos::NacosServiceDetail, String> {
|
||||
dbx_core::nacos::service::nacos_get_service_core(&state, &connection_id, query).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_create_service(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosServiceUpsert,
|
||||
) -> Result<(), String> {
|
||||
dbx_core::nacos::service::nacos_create_service_core(&state, &connection_id, req).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_update_service(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosServiceUpsert,
|
||||
) -> Result<(), String> {
|
||||
dbx_core::nacos::service::nacos_update_service_core(&state, &connection_id, req).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_delete_service(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
query: dbx_core::nacos::NacosServiceQuery,
|
||||
) -> Result<(), String> {
|
||||
dbx_core::nacos::service::nacos_delete_service_core(&state, &connection_id, query).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_list_instances(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
|
@ -142,11 +178,29 @@ pub async fn nacos_list_instances(
|
|||
pub async fn nacos_update_instance(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosInstanceUpdate,
|
||||
req: dbx_core::nacos::NacosInstanceUpdateRequest,
|
||||
) -> Result<(), String> {
|
||||
dbx_core::nacos::service::nacos_update_instance_core(&state, &connection_id, req).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_register_instance(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosInstanceRegistration,
|
||||
) -> Result<(), String> {
|
||||
dbx_core::nacos::service::nacos_register_instance_core(&state, &connection_id, req).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_deregister_instance(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
req: dbx_core::nacos::NacosInstanceRef,
|
||||
) -> Result<(), String> {
|
||||
dbx_core::nacos::service::nacos_deregister_instance_core(&state, &connection_id, req).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn nacos_get_dashboard(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -1755,8 +1755,14 @@ pub fn run() {
|
|||
commands::nacos_cmd::nacos_get_rnacos_console_captcha,
|
||||
commands::nacos_cmd::nacos_login_rnacos_console,
|
||||
commands::nacos_cmd::nacos_list_services,
|
||||
commands::nacos_cmd::nacos_get_service,
|
||||
commands::nacos_cmd::nacos_create_service,
|
||||
commands::nacos_cmd::nacos_update_service,
|
||||
commands::nacos_cmd::nacos_delete_service,
|
||||
commands::nacos_cmd::nacos_list_instances,
|
||||
commands::nacos_cmd::nacos_update_instance,
|
||||
commands::nacos_cmd::nacos_register_instance,
|
||||
commands::nacos_cmd::nacos_deregister_instance,
|
||||
commands::nacos_cmd::nacos_get_dashboard,
|
||||
commands::nacos_cmd::nacos_raw_request,
|
||||
commands::nacos_cmd::nacos_search_config_content,
|
||||
|
|
|
|||
Loading…
Reference in New Issue