feat(nacos): add Nacos management console (#1456)

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-06-24 01:19:18 +08:00
1 parent ff1559f018
commit 3ebfae05a2
62 files changed
+7314 -55

No files matched your search

@@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Switch } from "@/components/ui/switch";
import type { ConnectionConfig, DatabaseType, JdbcDriverInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
@@ -42,12 +43,17 @@ type DialogStep = "select" | "config";
type DbPickerView = "icon" | "list";
type ConfigTab = "connection" | "advanced" | "tls" | "transport";
type MqTokenSigningMode = "none" | "hs256" | "rs256";
type NacosAuthKind = NacosAuthConfig["kind"];
type JdbcDriverSelectItem = {
id: string;
label: string;
paths: string[];
};
const NACOS_DEFAULT_CONSOLE_URL = "http://127.0.0.1:8085";
const NACOS_LEGACY_SERVER_PORT = "8848";
const NACOS_DOCKER_CONSOLE_PORT = "8085";
type LegacyTransportFields = {
ssh_enabled?: boolean;
ssh_host?: string;
@@ -307,6 +313,14 @@ const mqTlsSkipVerify = ref(false);
const mqPinnedVersion = ref(pinnedVersionToSelection(undefined));
const mqTokenSigningMode = ref<MqTokenSigningMode>("none");
const mqTokenSigningKey = ref("");
const nacosServerAddr = ref(NACOS_DEFAULT_CONSOLE_URL);
const nacosNamespace = ref("");
const nacosContextPath = ref("");
const nacosAuthKind = ref<NacosAuthKind>("none");
const nacosUsername = ref("nacos");
const nacosPassword = ref("");
const nacosTlsSkipVerify = ref(false);
const nacosPageSize = ref(20);
const colorOptions = [
{ value: "", class: "bg-transparent border-dashed", labelKey: "connection.colorNone" },
@@ -509,6 +523,7 @@ const driverProfiles: Record<
iotdb: { type: "iotdb", port: 6667, user: "root", label: "Apache IoTDB", icon: "iotdb" },
etcd: { type: "etcd", port: 2379, user: "", label: "etcd", icon: "etcd" },
mq: { type: "mq", port: 8080, user: "", label: "Apache Pulsar", icon: "pulsar", host: "127.0.0.1" },
nacos: { type: "nacos", port: 8848, user: "nacos", label: "Nacos", icon: "nacos", host: "127.0.0.1" },
iris: { type: "iris", port: 1972, user: "_SYSTEM", label: "IRIS", icon: "iris" },
influxdb: { type: "influxdb", port: 8086, user: "", label: "InfluxDB", icon: "InfluxDB" },
custom_mysql: {
@@ -574,6 +589,26 @@ function hydrateMqFields(value: unknown) {
resetMqFields(value as Partial<MqAdminConfig>);
}
function resetNacosFields(config?: Partial<NacosAdminConfig>) {
nacosServerAddr.value = config?.serverAddr?.trim() || NACOS_DEFAULT_CONSOLE_URL;
nacosNamespace.value = config?.namespace || "";
nacosContextPath.value = config?.contextPath || "";
nacosTlsSkipVerify.value = !!config?.tlsSkipVerify;
nacosPageSize.value = Number(config?.pageSize) > 0 ? Number(config?.pageSize) : 20;
const auth = (config?.auth || { kind: "none" }) as NacosAuthConfig;
nacosAuthKind.value = auth.kind || "none";
nacosUsername.value = auth.username || "nacos";
nacosPassword.value = auth.password || "";
}
function hydrateNacosFields(value: unknown) {
if (!value || typeof value !== "object") {
resetNacosFields();
return;
}
resetNacosFields(value as Partial<NacosAdminConfig>);
}
function requireMqField(value: string, message: string): string {
const trimmed = value.trim();
if (!trimmed) throw new Error(message);
@@ -629,6 +664,65 @@ function buildMqAdminConfig(): MqAdminConfig {
};
}
function buildNacosAuth(): NacosAuthConfig {
if (nacosAuthKind.value === "usernamePassword") {
return {
kind: "usernamePassword",
username: requireMqField(nacosUsername.value, t("connection.nacosUsernameRequired")),
password: nacosPassword.value,
};
}
return { kind: "none" };
}
function buildNacosAdminConfig(): NacosAdminConfig {
return {
serverAddr: requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired")),
namespace: nacosNamespace.value.trim() || undefined,
contextPath: nacosContextPath.value.trim(),
auth: buildNacosAuth(),
tlsSkipVerify: nacosTlsSkipVerify.value || undefined,
pageSize: Number(nacosPageSize.value) > 0 ? Number(nacosPageSize.value) : 20,
};
}
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 !== NACOS_LEGACY_SERVER_PORT || !["127.0.0.1", "localhost", "::1"].includes(host)) {
return null;
}
parsed.port = NACOS_DOCKER_CONSOLE_PORT;
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): Promise<string | 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);
const message = await api.testConnection(fallbackConfig);
return `${message} ${t("connection.nacosConsoleUrlAutoAdjusted", { from: previousUrl.trim(), to: fallbackUrl })}`;
} catch {
nacosServerAddr.value = previousUrl;
return null;
}
}
function applyMqAdminUrl(config: LegacyConnectionConfig, adminUrl: string) {
let parsed: URL;
try {
@@ -642,6 +736,19 @@ function applyMqAdminUrl(config: LegacyConnectionConfig, adminUrl: string) {
config.ssl = parsed.protocol === "https:";
}
function applyNacosServerAddr(config: LegacyConnectionConfig, serverAddr: string) {
let parsed: URL;
try {
parsed = new URL(serverAddr);
} catch {
throw new Error("Nacos server address is invalid");
}
const port = Number(parsed.port) || (parsed.protocol === "https:" ? 443 : 8848);
config.host = parsed.hostname;
config.port = port;
config.ssl = parsed.protocol === "https:";
}
function isCustomCompatibleProfile() {
return selectedType.value === "custom_mysql" || selectedType.value === "custom_postgres";
}
@@ -691,6 +798,12 @@ function applyProfile(val: string, preserveConnectionFields = false) {
form.value.database = undefined;
form.value.connection_string = undefined;
}
if (profile.type === "nacos") {
resetNacosFields();
form.value.database = undefined;
form.value.connection_string = undefined;
form.value.url_params = "";
}
}
}
@@ -763,6 +876,11 @@ watch(
} else {
resetMqFields();
}
if (config.db_type === "nacos") {
hydrateNacosFields(config.external_config);
} else {
resetNacosFields();
}
h2ConnectionMode.value = h2ConnectionModeForConfig(config);
customColorInput.value = config.color || "";
selectedTransportLayerId.value = form.value.transport_layers?.[0]?.id || null;
@@ -786,6 +904,7 @@ watch(
selectedType.value = "mysql";
customDriverName.value = "";
resetMqFields();
resetNacosFields();
oceanbaseSubMode.value = "mysql";
h2ConnectionMode.value = "file";
dialogStep.value = "select";
@@ -922,6 +1041,7 @@ const iconTypeMap: Record<string, string> = {
iotdb: "iotdb",
etcd: "etcd",
mq: "mq",
nacos: "nacos",
dm: "dm",
h2: "h2",
snowflake: "snowflake",
@@ -1004,6 +1124,7 @@ const dbOptions: DbOption[] = [
{ value: "iotdb", label: "Apache IoTDB" },
{ value: "etcd", label: "etcd" },
{ value: "mq", label: "Apache Pulsar" },
{ value: "nacos", label: "Nacos" },
{ value: "influxdb", label: "InfluxDB" },
{ value: "iris", label: "IRIS" },
{ value: "jdbc", label: "JDBC" },
@@ -1160,6 +1281,7 @@ const testResultMessage = computed(() => {
});
const hasRequiredConnectionTarget = computed(() => {
if (form.value.db_type === "mq") return !!mqAdminUrl.value.trim();
if (form.value.db_type === "nacos") return !!nacosServerAddr.value.trim();
if (isH2FileMode.value) return !!(form.value.host.trim() || h2FilePathFromJdbcUrl(form.value.connection_string));
return !!(form.value.host || (mongoUseUrl.value && form.value.connection_string) || (form.value.db_type === "jdbc" && form.value.connection_string) || connectionUrlInput.value.trim());
});
@@ -1209,8 +1331,8 @@ async function testConnection() {
const runId = ++testRunId;
isTesting.value = true;
testResult.value = null;
const config = connectionConfigForSubmit(editingId.value || uuid());
try {
const config = connectionConfigForSubmit(editingId.value || uuid());
const msg = await api.testConnection(config);
if (runId !== testRunId) return;
if (config.db_type === "mongodb" && /legacy driver/i.test(msg)) {
@@ -1219,7 +1341,10 @@ async function testConnection() {
testResult.value = { ok: true, message: msg };
} catch (e: any) {
if (runId !== testRunId) return;
testResult.value = { ok: false, message: mongodbAuthFailureHint(String(e)) };
const message = mongodbAuthFailureHint(String(e));
const fallbackMessage = await tryNacosDockerConsoleFallback(config, message);
if (runId !== testRunId) return;
testResult.value = fallbackMessage ? { ok: true, message: fallbackMessage } : { ok: false, message };
} finally {
if (runId === testRunId) {
isTesting.value = false;
@@ -1301,6 +1426,15 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
config.database = undefined;
config.connection_string = undefined;
config.url_params = "";
} else if (config.db_type === "nacos") {
const nacosConfig = buildNacosAdminConfig();
config.external_config = nacosConfig;
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.connection_string = undefined;
config.url_params = "";
} else {
config.external_config = undefined;
}
@@ -2791,6 +2925,54 @@ function openExternalUrl(url: string) {
</div>
</template>
<!-- Nacos: server address, namespace and auth -->
<template v-else-if="form.db_type === 'nacos'">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.nacosConsoleUrl") }}</Label>
<Input v-model="nacosServerAddr" class="col-span-3" placeholder="http://127.0.0.1:8085" />
</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.nacosConsoleUrlHint") }}</p>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ 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="text-right">{{ t("connection.nacosContextPath") }}</Label>
<Input v-model="nacosContextPath" class="col-span-3" :placeholder="t('connection.nacosContextPathPlaceholder')" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ 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="text-right">{{ t("connection.user") }}</Label>
<Input v-model="nacosUsername" class="col-span-3" placeholder="nacos" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ 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="text-right text-xs">{{ 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="text-right">{{ t("connection.nacosPageSize") }}</Label>
<Input v-model.number="nacosPageSize" type="number" min="1" max="500" class="col-span-3" />
</div>
</template>
<!-- Redis: host, port, user, password, ssl -->
<template v-else-if="form.db_type === 'redis'">
<div class="grid grid-cols-4 items-center gap-4">