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

This commit is contained in:
二丫讲梵 2026-06-24 01:19:18 +08:00 committed by GitHub
parent ff1559f018
commit 3ebfae05a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 7314 additions and 55 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -765,6 +765,20 @@ async function openConnectionQuery(connectionId: string) {
queryStore.openMqAdmin(connectionId);
return;
}
if (initialTarget.kind === "nacos-admin") {
try {
await connectionStore.ensureConnected(connectionId);
await connectionStore.loadNacosNamespaces(connectionId);
} catch (e: any) {
toast(
t("connection.connectFailed", {
message: translateBackendError(t, e?.message || String(e)),
}),
5000,
);
}
return;
}
const tabId = queryStore.createTab(connectionId, initialTarget.database);
try {
await connectionStore.ensureConnected(connectionId);

View File

@ -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">

View File

@ -114,7 +114,7 @@ const showModified = ref(true);
let syncPlanRequestId = 0;
const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd"].includes(connection.db_type)));
const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd", "mq", "nacos"].includes(connection.db_type)));
const selectedSourceTableNames = computed(() => sourceTables.value.filter((table) => selectedSourceTables.value.has(table)));
const isBatchCompare = computed(() => selectedSourceTableNames.value.length > 1);
const filteredSourceTables = computed(() => {

View File

@ -53,7 +53,7 @@ const targetSchemas = ref<string[]>([]);
const sourceDbVersion = ref<string | null>(null);
const targetDbVersion = ref<string | null>(null);
const sqlConnections = computed(() => store.connections.filter((c: any) => c.db_type !== "mongodb" && c.db_type !== "redis"));
const sqlConnections = computed(() => store.connections.filter((c: any) => !["mongodb", "redis", "elasticsearch", "etcd", "mq", "nacos"].includes(c.db_type)));
const sourceConfig = computed(() => store.getConfig(props.sourceConnectionId));
const targetConfig = computed(() => store.getConfig(props.targetConnectionId));

View File

@ -8,6 +8,7 @@ import { ChevronUp, ChevronDown, ChevronRight, X } from "@lucide/vue";
const props = defineProps<{
view: EditorView | null;
tone?: "app" | "editor";
}>();
const { t } = useI18n();
@ -214,56 +215,224 @@ defineExpose({ openSearch, openReplace, closeSearch });
<template>
<Transition enter-active-class="transition-[transform,opacity] duration-150" leave-active-class="transition-[transform,opacity] duration-100" enter-from-class="opacity-0 -translate-y-1" leave-to-class="opacity-0 -translate-y-1">
<div v-if="searchVisible" class="absolute top-1 right-4 z-[9999] isolate flex flex-col gap-1 rounded-md border bg-popover p-1.5 text-popover-foreground shadow-lg">
<div class="flex items-center gap-0.5">
<button class="w-5 h-5 flex items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground" :title="showReplace ? t('editor.search.collapseReplace') : t('editor.search.expandReplace')" @click="showReplace = !showReplace">
<ChevronRight class="w-3 h-3 transition-transform" :class="showReplace && 'rotate-90'" />
<div v-if="searchVisible" class="editor-search-panel absolute right-4 top-3 z-[9999] isolate flex flex-col gap-1 rounded-lg border border-border bg-popover p-1.5 text-popover-foreground shadow-xl ring-1 ring-border/60" :class="{ 'editor-search-panel--editor': tone === 'editor' }">
<div class="flex items-center gap-1">
<button class="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" :title="showReplace ? t('editor.search.collapseReplace') : t('editor.search.expandReplace')" @click="showReplace = !showReplace">
<ChevronRight class="h-4 w-4 transition-transform" :class="showReplace && 'rotate-90'" />
</button>
<input
ref="searchInputRef"
v-model="searchText"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="w-48 h-6 text-xs bg-input border rounded px-2 outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground"
:placeholder="t('editor.search.find')"
@keydown="onSearchKeydown"
/>
<button class="w-6 h-6 flex items-center justify-center rounded text-xs font-mono hover:bg-accent" :class="caseSensitive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'" :title="t('editor.search.caseSensitive')" @click="caseSensitive = !caseSensitive">Aa</button>
<button class="w-6 h-6 flex items-center justify-center rounded text-xs font-mono hover:bg-accent" :class="useRegex ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'" :title="t('editor.search.regex')" @click="useRegex = !useRegex">.*</button>
<span class="text-xs text-muted-foreground min-w-[3rem] text-center shrink-0">
<div class="flex h-8 w-64 items-center rounded-md border border-input bg-background focus-within:border-ring focus-within:ring-1 focus-within:ring-ring">
<input
ref="searchInputRef"
v-model="searchText"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="h-full min-w-0 flex-1 bg-transparent px-2 text-sm text-foreground outline-none placeholder:text-muted-foreground"
:placeholder="t('editor.search.find')"
@keydown="onSearchKeydown"
/>
<button
class="flex h-6 min-w-7 items-center justify-center rounded px-1.5 text-xs font-medium transition-colors hover:bg-accent hover:text-foreground"
:class="caseSensitive ? 'bg-accent text-foreground' : 'text-muted-foreground'"
:title="t('editor.search.caseSensitive')"
@click="caseSensitive = !caseSensitive"
>
Aa
</button>
<button
class="mr-1 flex h-6 min-w-7 items-center justify-center rounded px-1.5 font-mono text-xs transition-colors hover:bg-accent hover:text-foreground"
:class="useRegex ? 'bg-accent text-foreground' : 'text-muted-foreground'"
:title="t('editor.search.regex')"
@click="useRegex = !useRegex"
>
.*
</button>
</div>
<span class="min-w-[3.4rem] shrink-0 text-center text-xs" :class="searchText && matchCount === 0 ? 'text-destructive' : 'text-muted-foreground'">
{{ searchText && matchCount > 0 ? `${currentMatchIndex}/${matchCount}${matchCountLimited ? "+" : ""}` : t("editor.search.noResults") }}
</span>
<button class="w-5 h-5 flex items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('editor.search.prevMatch')" @click="prevMatch">
<ChevronUp class="w-3.5 h-3.5" />
<button class="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" :title="t('editor.search.prevMatch')" @click="prevMatch">
<ChevronUp class="h-4 w-4" />
</button>
<button class="w-5 h-5 flex items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('editor.search.nextMatch')" @click="nextMatch">
<ChevronDown class="w-3.5 h-3.5" />
<button class="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" :title="t('editor.search.nextMatch')" @click="nextMatch">
<ChevronDown class="h-4 w-4" />
</button>
<button class="w-5 h-5 flex items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('editor.search.close')" @click="closeSearch">
<X class="w-3.5 h-3.5" />
<button class="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" :title="t('editor.search.close')" @click="closeSearch">
<X class="h-4 w-4" />
</button>
</div>
<div v-if="showReplace" class="flex items-center gap-0.5">
<div class="w-5 h-5 shrink-0" />
<input
ref="replaceInputRef"
v-model="replaceText"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="w-48 h-6 text-xs bg-input border rounded px-2 outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground"
:placeholder="t('editor.search.replace')"
@keydown.enter.prevent="doReplace"
@keydown.escape.prevent="closeSearch"
/>
<button class="h-6 px-1.5 flex items-center justify-center rounded text-xs text-muted-foreground hover:bg-accent hover:text-foreground border" :title="t('editor.search.replace')" @click="doReplace">
<div v-if="showReplace" class="flex items-center gap-1">
<div class="h-7 w-7 shrink-0" />
<div class="flex h-8 w-64 items-center rounded-md border border-input bg-background focus-within:border-ring focus-within:ring-1 focus-within:ring-ring">
<input
ref="replaceInputRef"
v-model="replaceText"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="h-full min-w-0 flex-1 bg-transparent px-2 text-sm text-foreground outline-none placeholder:text-muted-foreground"
:placeholder="t('editor.search.replace')"
@keydown.enter.prevent="doReplace"
@keydown.escape.prevent="closeSearch"
/>
</div>
<button class="flex h-7 items-center justify-center rounded-md border border-border px-2 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" :title="t('editor.search.replace')" @click="doReplace">
{{ t("editor.search.replace") }}
</button>
<button class="h-6 px-1.5 flex items-center justify-center rounded text-xs text-muted-foreground hover:bg-accent hover:text-foreground border" :title="t('editor.search.replaceAll')" @click="doReplaceAll">
<button class="flex h-7 items-center justify-center rounded-md border border-border px-2 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" :title="t('editor.search.replaceAll')" @click="doReplaceAll">
{{ t("editor.search.replaceAll") }}
</button>
</div>
</div>
</Transition>
</template>
<style scoped>
.editor-search-panel {
max-width: min(calc(100vw - 2rem), 620px);
}
.editor-search-panel--editor {
background: #f6f7f9;
border-color: #d8dce3;
border-radius: 6px;
box-shadow:
0 8px 22px rgb(15 23 42 / 0.14),
0 1px 0 rgb(255 255 255 / 0.78) inset;
color: #20242a;
gap: 3px;
max-width: min(calc(100vw - 2rem), 500px);
padding: 4px 6px;
right: 0.75rem;
top: 0.75rem;
}
.editor-search-panel--editor :deep(.h-8) {
height: 27px;
}
.editor-search-panel--editor :deep(.h-7) {
height: 27px;
}
.editor-search-panel--editor :deep(.w-7) {
width: 27px;
}
.editor-search-panel--editor :deep(.w-64) {
width: 230px;
}
.editor-search-panel--editor :deep(button) {
font-size: 12px;
}
.editor-search-panel--editor :deep(button.px-2) {
padding-left: 7px;
padding-right: 7px;
}
.editor-search-panel--editor :deep(.min-w-\[3\.4rem\]) {
min-width: 3.25rem;
}
.editor-search-panel--editor :deep(.border-input) {
background: #ffffff;
border-color: #c7ccd5;
border-radius: 5px;
box-shadow: 0 1px 0 rgb(15 23 42 / 0.03) inset;
}
.editor-search-panel--editor :deep(.focus-within\:border-ring:focus-within) {
border-color: #3b82f6;
box-shadow: 0 0 0 1px #3b82f6;
}
.editor-search-panel--editor :deep(input) {
color: #20242a;
font-size: 12px;
}
.editor-search-panel--editor :deep(input::placeholder) {
color: #7a828e;
}
.editor-search-panel--editor :deep(.text-muted-foreground) {
color: #6e7681;
}
.editor-search-panel--editor :deep(.text-destructive) {
color: #c2410c;
}
.editor-search-panel--editor :deep(.hover\:bg-accent:hover),
.editor-search-panel--editor :deep(.bg-accent) {
background: #e6e9ef;
}
.editor-search-panel--editor :deep(.hover\:text-foreground:hover),
.editor-search-panel--editor :deep(.text-foreground) {
color: #1f2329;
}
.editor-search-panel--editor :deep(button.border-border) {
border-color: #d0d5dd;
}
:global(.dark) .editor-search-panel--editor {
background: #20252d;
border-color: #2c333d;
box-shadow:
0 8px 24px rgb(0 0 0 / 0.28),
0 1px 0 rgb(255 255 255 / 0.04) inset;
color: #d4d7dc;
}
:global(.dark) .editor-search-panel--editor :deep(.border-input) {
background: #191d25;
border-color: #2f3742;
box-shadow: none;
}
:global(.dark) .editor-search-panel--editor :deep(.focus-within\:border-ring:focus-within) {
border-color: #4d8dff;
box-shadow: 0 0 0 1px #4d8dff;
}
:global(.dark) .editor-search-panel--editor :deep(input) {
color: #d7dae0;
}
:global(.dark) .editor-search-panel--editor :deep(input::placeholder) {
color: #858c97;
}
:global(.dark) .editor-search-panel--editor :deep(.text-muted-foreground) {
color: #9aa2ad;
}
:global(.dark) .editor-search-panel--editor :deep(.text-destructive) {
color: #f59e7a;
}
:global(.dark) .editor-search-panel--editor :deep(.hover\:bg-accent:hover),
:global(.dark) .editor-search-panel--editor :deep(.bg-accent) {
background: #303844;
}
:global(.dark) .editor-search-panel--editor :deep(.hover\:text-foreground:hover),
:global(.dark) .editor-search-panel--editor :deep(.text-foreground) {
color: #f2f4f8;
}
:global(.dark) .editor-search-panel--editor :deep(button.border-border) {
border-color: #38414d;
}
@media (max-width: 720px) {
.editor-search-panel {
left: 0.75rem;
right: 0.75rem;
}
}
</style>

View File

@ -65,7 +65,7 @@ const exportCancelled = ref(false);
const pendingPrefillTable = ref("");
const pendingPrefillTables = ref<string[]>([]);
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd"].includes(c.db_type)));
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd", "mq", "nacos"].includes(c.db_type)));
const canExport = computed(() => connectionId.value && database.value && schema.value && !loadingTables.value && !tableError.value && (tables.value.length === 0 || selectedTables.value.length > 0) && (includeStructure.value || includeData.value || includeObjects.value) && !isExporting.value);

View File

@ -81,6 +81,7 @@ const assetIcons: Record<string, string> = {
milvus: "milvus.png",
mq: "pulsar",
pulsar: "pulsar",
nacos: "nacos.png",
iris: "iris.png",
influxdb: "influxdb",
};

View File

@ -2,7 +2,7 @@
import { computed, ref, watch, nextTick } from "vue";
import type { CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock, Copy, AlertTriangle } from "@lucide/vue";
import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock, Copy, AlertTriangle, Network } from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import LightDropdown from "@/components/ui/LightDropdown.vue";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
@ -236,6 +236,7 @@ function tabMenuIcon(tab: QueryTab) {
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis") return Table2;
if (tab.mode === "vector") return TableProperties;
if (tab.mode === "etcd") return KeyRound;
if (tab.mode === "nacos") return Network;
if (tab.mode === "objects") return TableProperties;
if (tab.mode === "structure") return PencilRuler;
return Code2;
@ -329,6 +330,7 @@ function activateTab(tabId: string) {
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
<KeyRound v-else-if="tab.mode === 'etcd'" class="h-3.5 w-3.5" />
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />

View File

@ -38,6 +38,7 @@ const EtcdKeyBrowser = defineAsyncComponent(() => import("@/components/etcd/Etcd
const DocumentBrowser = defineAsyncComponent(() => import("@/components/document/DocumentBrowser.vue"));
const VectorBrowser = defineAsyncComponent(() => import("@/components/vector/VectorBrowser.vue"));
const MqAdminConsole = defineAsyncComponent(() => import("@/components/mq/MqAdminConsole.vue"));
const NacosAdminConsole = defineAsyncComponent(() => import("@/components/nacos/NacosAdminConsole.vue"));
const ObjectBrowser = defineAsyncComponent(() => import("@/components/objects/ObjectBrowser.vue"));
const TableStructureEditor = defineAsyncComponent(() => import("@/components/structure/TableStructureEditor.vue"));
const DatabaseUserAdmin = defineAsyncComponent(() => import("@/components/admin/DatabaseUserAdmin.vue"));
@ -1029,6 +1030,12 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
</div>
</template>
<template v-else-if="activeTab.mode === 'nacos'">
<div class="flex-1 min-h-0">
<NacosAdminConsole :key="activeTab.id" :connection-id="activeTab.connectionId" :namespace="activeTab.nacosNamespace" :namespace-name="activeTab.nacosNamespaceName" :read-only="activeConnection?.read_only ?? false" />
</div>
</template>
<!-- Objects mode: virtualized database object browser -->
<template v-else-if="activeTab.mode === 'objects' && activeConnection">
<ObjectBrowser

View File

@ -61,7 +61,7 @@ const activeConnectionValue = computed(() => props.activeConnection?.id || "");
const activeSchemaValue = computed(() => props.activeTab.schema || "");
const supportsExplain = computed(() => {
const dbType = props.activeConnection?.db_type;
return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd";
return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos";
});
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
const hasDefaultDatabaseOption = computed(() => activeDatabaseOptions.value.includes(""));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,209 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { Loader2 } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { buildNacosInlineDiff, buildNacosSideBySideDiff, summarizeNacosConfigDiff, type NacosDiffLineType, type NacosInlineSegment } from "@/lib/nacosAdmin";
const open = defineModel<boolean>("open", { default: false });
const props = withDefaults(
defineProps<{
before: string;
after: string;
title?: string;
beforeLabel?: string;
afterLabel?: string;
confirmLabel?: string;
confirmVariant?: "default" | "destructive";
showConfirm?: boolean;
loading?: boolean;
}>(),
{
title: "",
beforeLabel: "",
afterLabel: "",
confirmLabel: "",
confirmVariant: "default",
showConfirm: true,
loading: false,
},
);
const emit = defineEmits<{
confirm: [];
}>();
const { t } = useI18n();
const inlineCompare = ref(false);
const rows = computed(() => buildNacosSideBySideDiff(props.before, props.after));
const inlineRows = computed(() => buildNacosInlineDiff(props.before, props.after));
const summary = computed(() => summarizeNacosConfigDiff(props.before, props.after));
const dialogOpen = computed({
get: () => open.value,
set: (value) => {
if (props.loading && !value) return;
open.value = value;
},
});
function lineClass(type: NacosDiffLineType, side: "left" | "right") {
return {
"bg-red-500/20 text-red-50": type === "delete" || (type === "modify" && side === "left"),
"bg-emerald-500/18 text-emerald-50": type === "insert" || (type === "modify" && side === "right"),
"text-zinc-200": type === "equal",
"bg-zinc-900/80 text-zinc-600": type === "padding",
};
}
function gutterClass(type: NacosDiffLineType, side: "left" | "right") {
return {
"text-red-300": type === "delete" || (type === "modify" && side === "left"),
"text-emerald-300": type === "insert" || (type === "modify" && side === "right"),
"text-zinc-500": type === "equal" || type === "padding",
};
}
function prefix(type: NacosDiffLineType, side: "left" | "right") {
if (type === "delete" || (type === "modify" && side === "left")) return "-";
if (type === "insert" || (type === "modify" && side === "right")) return "+";
return "";
}
function inlineSegments(content: string, segments: NacosInlineSegment[]) {
return segments.length ? segments : [{ value: content, changed: false }];
}
function inlineClass(type: NacosDiffLineType, changed: boolean) {
if (!changed) return "";
if (type === "delete" || type === "modify") return "nacos-inline-change rounded-[2px] bg-red-500/80 text-red-50";
if (type === "insert") return "nacos-inline-change rounded-[2px] bg-emerald-500/75 text-emerald-50";
return "";
}
function rightInlineClass(type: NacosDiffLineType, changed: boolean) {
if (!changed) return "";
if (type === "modify" || type === "insert") return "nacos-inline-change rounded-[2px] bg-emerald-500/75 text-emerald-50";
return inlineClass(type, changed);
}
function inlineRowClass(type: "equal" | "delete" | "insert") {
return {
"bg-red-500/20 text-red-50": type === "delete",
"bg-emerald-500/18 text-emerald-50": type === "insert",
"text-zinc-200": type === "equal",
};
}
function inlineGutterClass(type: "equal" | "delete" | "insert") {
return {
"text-red-300": type === "delete",
"text-emerald-300": type === "insert",
"text-zinc-500": type === "equal",
};
}
function inlinePrefix(type: "equal" | "delete" | "insert") {
if (type === "delete") return "-";
if (type === "insert") return "+";
return "";
}
function inlineRowSegmentClass(type: "equal" | "delete" | "insert", changed: boolean) {
if (!changed) return "";
if (type === "delete") return "nacos-inline-change rounded-[2px] bg-red-500/80 text-red-50";
if (type === "insert") return "nacos-inline-change rounded-[2px] bg-emerald-500/75 text-emerald-50";
return "";
}
function onConfirm() {
if (props.loading) return;
emit("confirm");
}
</script>
<template>
<Dialog v-model:open="dialogOpen">
<DialogContent :show-close-button="false" class="nacos-config-diff-dialog flex h-[min(88vh,900px)] flex-col gap-0 overflow-hidden rounded-lg p-0 shadow-2xl">
<DialogHeader class="shrink-0 border-b px-5 py-4">
<div class="flex items-center justify-between gap-4">
<DialogTitle class="text-lg font-semibold">{{ title || t("nacos.configDiffTitle") }}</DialogTitle>
<button type="button" class="text-2xl leading-none text-muted-foreground hover:text-foreground" :disabled="loading" @click="open = false">×</button>
</div>
<div class="mt-3 flex flex-wrap items-center justify-between gap-3">
<label class="inline-flex items-center gap-2 text-sm text-muted-foreground">
<input v-model="inlineCompare" type="checkbox" class="h-4 w-4 rounded border-border" />
<span>{{ t("nacos.inlineCompare") }}</span>
</label>
<div class="text-xs text-muted-foreground">{{ t("nacos.confirmSaveMessage", { added: summary.addedLines, removed: summary.removedLines }) }}</div>
</div>
</DialogHeader>
<div v-if="!inlineCompare" class="grid min-h-0 flex-1 grid-cols-1 gap-4 bg-background px-5 py-4 lg:grid-cols-2">
<section class="flex min-w-0 min-h-0 flex-col">
<div class="mb-2 text-sm font-medium text-foreground">{{ beforeLabel || t("nacos.currentVersionContent") }}</div>
<div class="min-h-0 flex-1 overflow-auto rounded-sm border border-zinc-700 bg-[#1f1f1f] font-mono text-[13px] leading-6 text-zinc-200">
<div v-for="row in rows" :key="`${row.id}-left`" class="grid min-w-max grid-cols-[52px_22px_minmax(48rem,1fr)]" :class="lineClass(row.leftType, 'left')">
<span class="select-none border-r border-white/8 pr-2 text-right" :class="gutterClass(row.leftType, 'left')">{{ row.leftLineNumber ?? "" }}</span>
<span class="select-none pl-2" :class="gutterClass(row.leftType, 'left')">{{ prefix(row.leftType, "left") }}</span>
<pre class="whitespace-pre px-2"><template v-for="(segment, index) in inlineSegments(row.leftContent, row.leftInline)" :key="index"><span :class="inlineClass(row.leftType, segment.changed)">{{ segment.value }}</span></template></pre>
</div>
</div>
</section>
<section class="flex min-w-0 min-h-0 flex-col">
<div class="mb-2 text-sm font-medium text-foreground">{{ afterLabel || t("nacos.publishVersionContent") }}</div>
<div class="min-h-0 flex-1 overflow-auto rounded-sm border border-zinc-700 bg-[#1f1f1f] font-mono text-[13px] leading-6 text-zinc-200">
<div v-for="row in rows" :key="`${row.id}-right`" class="grid min-w-max grid-cols-[52px_22px_minmax(48rem,1fr)]" :class="lineClass(row.rightType, 'right')">
<span class="select-none border-r border-white/8 pr-2 text-right" :class="gutterClass(row.rightType, 'right')">{{ row.rightLineNumber ?? "" }}</span>
<span class="select-none pl-2" :class="gutterClass(row.rightType, 'right')">{{ prefix(row.rightType, "right") }}</span>
<pre class="whitespace-pre px-2"><template v-for="(segment, index) in inlineSegments(row.rightContent, row.rightInline)" :key="index"><span :class="rightInlineClass(row.rightType, segment.changed)">{{ segment.value }}</span></template></pre>
</div>
</div>
</section>
</div>
<div v-else class="min-h-0 flex-1 bg-background px-5 py-4">
<section class="flex h-full min-h-0 flex-col">
<div class="mb-2 text-sm font-medium text-foreground">{{ t("nacos.inlineCompare") }}</div>
<div class="min-h-0 flex-1 overflow-auto rounded-sm border border-zinc-700 bg-[#1f1f1f] font-mono text-[13px] leading-6 text-zinc-200">
<div v-for="row in inlineRows" :key="row.id" class="grid min-w-max grid-cols-[52px_22px_minmax(96rem,1fr)]" :class="inlineRowClass(row.type)">
<span class="select-none border-r border-white/8 pr-2 text-right" :class="inlineGutterClass(row.type)">{{ row.lineNumber ?? "" }}</span>
<span class="select-none pl-2" :class="inlineGutterClass(row.type)">{{ inlinePrefix(row.type) }}</span>
<pre class="whitespace-pre px-2"><template v-for="(segment, index) in row.segments" :key="index"><span :class="inlineRowSegmentClass(row.type, segment.changed)">{{ segment.value }}</span></template></pre>
</div>
</div>
</section>
</div>
<DialogFooter class="shrink-0 gap-3 border-t bg-muted/20 px-5 pb-6 pt-4">
<Button v-if="showConfirm" :variant="confirmVariant" class="min-w-24 gap-1.5 px-5" :disabled="loading" @click="onConfirm">
<Loader2 v-if="loading" class="h-3.5 w-3.5 animate-spin" />
{{ confirmLabel || t("nacos.publish") }}
</Button>
<Button variant="outline" class="min-w-24 px-5" :disabled="loading" @click="open = false">{{ t("dangerDialog.cancel") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<style>
.nacos-config-diff-dialog {
width: min(96vw, 1440px) !important;
max-width: min(96vw, 1440px) !important;
}
.nacos-inline-change {
box-decoration-break: clone;
padding: 0 1px;
}
@media (max-width: 1023px) {
.nacos-config-diff-dialog {
width: min(96vw, 760px) !important;
max-width: min(96vw, 760px) !important;
}
}
</style>

View File

@ -0,0 +1,224 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { Clock3, Eye, FileText, GitCompare, Loader2, RefreshCw, RotateCcw, X } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import type { NacosConfigHistoryItem, NacosConfigItem } from "@/types/nacos";
const open = defineModel<boolean>("open", { default: false });
const props = withDefaults(
defineProps<{
config: NacosConfigItem | null;
items: NacosConfigHistoryItem[];
loading?: boolean;
error?: string;
pageNo: number;
pageSize: number;
totalCount: number;
readOnly?: boolean;
viewingItem?: NacosConfigHistoryItem | null;
viewingContent?: string;
viewingLoading?: boolean;
}>(),
{
items: () => [],
loading: false,
error: "",
readOnly: false,
viewingItem: null,
viewingContent: "",
viewingLoading: false,
},
);
const emit = defineEmits<{
load: [pageNo: number];
view: [item: NacosConfigHistoryItem];
"close-detail": [];
compare: [item: NacosConfigHistoryItem];
rollback: [item: NacosConfigHistoryItem];
}>();
const { t } = useI18n();
const detailOpen = ref(false);
const totalPages = computed(() => Math.max(1, Math.ceil(props.totalCount / Math.max(1, props.pageSize))));
const historyTitle = computed(() => {
if (!props.config) return t("nacos.configHistory");
return `${props.config.dataId} / ${props.config.group || "DEFAULT_GROUP"}`;
});
const namespaceLabel = computed(() => props.config?.namespace || "public");
const dataIdLabel = computed(() => props.config?.dataId || "-");
const groupLabel = computed(() => props.config?.group || "DEFAULT_GROUP");
watch(
() => props.viewingItem,
(item) => {
if (item) detailOpen.value = true;
},
);
watch(detailOpen, (value) => {
if (!value) emit("close-detail");
});
function display(value?: string | null) {
const trimmed = value?.trim();
return trimmed || "-";
}
function operationLabel(value?: string) {
const normalized = value?.trim().toLowerCase();
if (!normalized) return "-";
if (["i", "insert", "create", "add"].includes(normalized)) return t("nacos.historyCreate");
if (["u", "update", "modify", "publish"].includes(normalized)) return t("nacos.historyUpdate");
if (["d", "delete", "remove"].includes(normalized)) return t("nacos.historyDelete");
if (["rollback", "recover"].includes(normalized)) return t("nacos.historyRollback");
return value || "-";
}
function loadPage(pageNo: number) {
if (props.loading || pageNo < 1 || pageNo > totalPages.value) return;
emit("load", pageNo);
}
</script>
<template>
<Dialog v-model:open="open">
<DialogContent :show-close-button="false" class="nacos-config-history-dialog flex h-[min(82vh,760px)] flex-col gap-0 overflow-hidden rounded-lg p-0 shadow-2xl">
<DialogHeader class="shrink-0 border-b bg-muted/20 px-5 py-4">
<div class="flex items-center justify-between gap-4">
<div class="flex min-w-0 items-start gap-3">
<div class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md border bg-background text-primary">
<Clock3 class="h-4 w-4" />
</div>
<div class="min-w-0">
<DialogTitle class="truncate text-lg font-semibold">{{ t("nacos.configHistory") }}</DialogTitle>
<div class="mt-2 flex min-w-0 flex-wrap items-center gap-1.5 text-xs">
<Badge variant="outline" class="max-w-64 truncate font-mono">namespace={{ namespaceLabel }}</Badge>
<Badge variant="secondary" class="max-w-72 truncate font-mono">dataId={{ dataIdLabel }}</Badge>
<Badge variant="outline" class="max-w-48 truncate font-mono">group={{ groupLabel }}</Badge>
</div>
<div class="sr-only">{{ historyTitle }}</div>
</div>
</div>
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" @click="open = false">
<X class="h-4 w-4" />
</Button>
</div>
</DialogHeader>
<div v-if="error" class="shrink-0 border-b px-5 py-2 text-xs text-destructive">{{ error }}</div>
<div class="min-h-0 flex-1 overflow-auto">
<table class="w-full text-left text-sm">
<thead class="sticky top-0 z-10 bg-muted/85 text-xs text-muted-foreground">
<tr>
<th class="px-4 py-2 font-medium">Data ID</th>
<th class="px-4 py-2 font-medium">Group</th>
<th class="px-4 py-2 font-medium">{{ t("nacos.updatedAt") }}</th>
<th class="px-4 py-2 font-medium">{{ t("nacos.application") }}</th>
<th class="px-4 py-2 font-medium">{{ t("nacos.operationType") }}</th>
<th class="px-4 py-2 font-medium">{{ t("nacos.operator") }}</th>
<th class="px-4 py-2 text-right font-medium">{{ t("nacos.actions") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="`${item.historyId}:${item.nid ?? ''}`" class="border-b">
<td class="max-w-72 truncate px-4 py-2 font-medium" :title="item.dataId">{{ item.dataId }}</td>
<td class="max-w-44 truncate px-4 py-2 text-xs text-muted-foreground" :title="item.group">{{ item.group || "DEFAULT_GROUP" }}</td>
<td class="whitespace-nowrap px-4 py-2 text-xs text-muted-foreground">{{ display(item.lastModifiedTime) }}</td>
<td class="max-w-40 truncate px-4 py-2 text-xs text-muted-foreground" :title="item.appName || ''">{{ display(item.appName) }}</td>
<td class="px-4 py-2">
<Badge variant="outline">{{ operationLabel(item.operation) }}</Badge>
</td>
<td class="max-w-40 truncate px-4 py-2 text-xs text-muted-foreground" :title="item.operator || ''">{{ display(item.operator) }}</td>
<td class="px-4 py-2 text-right">
<div class="inline-flex items-center gap-1.5">
<Button size="sm" variant="ghost" class="h-7 gap-1 px-2" :disabled="loading" @click="emit('view', item)">
<Eye class="h-3.5 w-3.5" />
{{ t("nacos.view") }}
</Button>
<Button size="sm" variant="ghost" class="h-7 gap-1 px-2" :disabled="loading" @click="emit('compare', item)">
<GitCompare class="h-3.5 w-3.5" />
{{ t("nacos.compare") }}
</Button>
<Button size="sm" variant="ghost" class="h-7 gap-1 px-2 text-destructive hover:text-destructive" :disabled="readOnly || loading" @click="emit('rollback', item)">
<RotateCcw class="h-3.5 w-3.5" />
{{ t("nacos.rollback") }}
</Button>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="loading" class="flex h-44 items-center justify-center text-sm text-muted-foreground">
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{{ t("nacos.loadingHistory") }}
</div>
<div v-else-if="items.length === 0" class="flex h-52 flex-col items-center justify-center gap-2 text-sm text-muted-foreground">
<div class="flex h-10 w-10 items-center justify-center rounded-md border bg-muted/30">
<FileText class="h-4 w-4" />
</div>
<div class="font-medium text-foreground">{{ t("nacos.noHistory") }}</div>
<div class="text-xs">{{ t("nacos.noHistoryHint") }}</div>
</div>
</div>
<DialogFooter class="shrink-0 items-center justify-between gap-3 border-t bg-muted/20 px-5 pb-5 pt-4">
<Button variant="outline" size="sm" class="h-8 gap-1.5" :disabled="loading" @click="loadPage(pageNo)">
<RefreshCw class="h-3.5 w-3.5" />
{{ t("nacos.refresh") }}
</Button>
<div class="flex items-center gap-2 rounded-md border bg-background px-2 py-1 text-xs text-muted-foreground">
<span>{{ t("nacos.total", { count: totalCount }) }}</span>
<Button size="sm" variant="outline" class="h-7" :disabled="pageNo <= 1 || loading" @click="loadPage(pageNo - 1)">{{ t("nacos.prev") }}</Button>
<span>{{ pageNo }} / {{ totalPages }}</span>
<Button size="sm" variant="outline" class="h-7" :disabled="pageNo >= totalPages || loading" @click="loadPage(pageNo + 1)">{{ t("nacos.next") }}</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="detailOpen">
<DialogContent class="nacos-config-history-detail-dialog flex h-[min(88vh,900px)] flex-col gap-0 overflow-hidden p-0">
<DialogHeader class="shrink-0 border-b px-5 py-4">
<DialogTitle class="truncate text-base font-semibold">{{ t("nacos.historyDetail") }}</DialogTitle>
<div v-if="viewingItem" class="mt-2 grid gap-1.5 text-xs text-muted-foreground sm:grid-cols-2">
<div class="min-w-0 truncate font-mono" :title="viewingItem.namespace || 'public'">namespace={{ viewingItem.namespace || "public" }}</div>
<div class="min-w-0 truncate font-mono" :title="viewingItem.dataId">dataId={{ viewingItem.dataId }}</div>
<div class="min-w-0 truncate font-mono" :title="viewingItem.group || 'DEFAULT_GROUP'">group={{ viewingItem.group || "DEFAULT_GROUP" }}</div>
<div class="min-w-0 truncate" :title="display(viewingItem.lastModifiedTime)">{{ t("nacos.updatedAt") }}={{ display(viewingItem.lastModifiedTime) }}</div>
</div>
</DialogHeader>
<div class="min-h-0 flex-1 overflow-auto bg-muted/20 p-4">
<div v-if="viewingLoading" class="flex h-full items-center justify-center text-sm text-muted-foreground">
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{{ t("nacos.loadingHistory") }}
</div>
<pre v-else class="min-h-full rounded-md border bg-background p-3 font-mono text-xs leading-5">{{ viewingContent || "" }}</pre>
</div>
<DialogFooter class="m-0 shrink-0 rounded-none border-t bg-background px-5 py-5 sm:py-4">
<Button variant="outline" @click="detailOpen = false">{{ t("dangerDialog.cancel") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<style>
.nacos-config-history-dialog {
width: min(96vw, 1280px) !important;
max-width: min(96vw, 1280px) !important;
}
.nacos-config-history-detail-dialog {
width: min(92vw, 1180px) !important;
max-width: min(92vw, 1180px) !important;
}
.nacos-config-history-detail-dialog pre {
min-width: max-content;
white-space: pre;
}
</style>

View File

@ -81,7 +81,7 @@ const isFiltering = computed(() => !!searchQuery.value.trim() || hasSearchScopeF
const SEARCH_SCOPE_TO_NODE_TYPES: Record<SearchScope, TreeNodeType[]> = {
connection: ["connection"],
database: ["database", "redis-db", "mq-tenant", "mongo-db"],
database: ["database", "redis-db", "mq-tenant", "nacos-namespace", "mongo-db"],
schema: ["schema"],
table: ["table", "mongo-collection", "vector-collection", "elasticsearch-index"],
view: ["view"],
@ -340,6 +340,8 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
await store.loadVectorCollections(connId);
} else if (config.db_type === "mq") {
await store.loadMqTenants(connId, loadOptions);
} else if (config.db_type === "nacos") {
await store.loadNacosNamespaces(connId, loadOptions);
} else {
await store.loadDatabases(connId, loadOptions);
}
@ -348,7 +350,7 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
}
}
if (config.db_type === "mq") return;
if (config.db_type === "mq" || config.db_type === "nacos") return;
if (!("database" in target) || !target.database) return;
// Find the database node

View File

@ -246,6 +246,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Database, colorClass: "text-red-400" };
case "mq-tenant":
return { icon: FolderOpen, colorClass: "text-sky-400" };
case "nacos-namespace":
return { icon: FolderOpen, colorClass: "text-sky-500" };
case "etcd-root":
return { icon: Database, colorClass: "text-sky-500" };
case "mongo-db":
@ -468,6 +470,8 @@ async function toggle() {
await connectionStore.loadVectorCollections(node.connectionId);
} else if (config?.db_type === "mq") {
await connectionStore.loadMqTenants(node.connectionId);
} else if (config?.db_type === "nacos") {
await connectionStore.loadNacosNamespaces(node.connectionId);
} else {
await connectionStore.loadDatabases(node.connectionId);
}
@ -476,6 +480,8 @@ async function toggle() {
queryStore.createTab(node.connectionId, node.database, tabTitle, "redis");
} else if (node.type === "mq-tenant" && node.connectionId) {
queryStore.openMqAdmin(node.connectionId, { tenant: node.mqTenant || node.label });
} else if (node.type === "nacos-namespace" && node.connectionId) {
queryStore.openNacosAdmin(node.connectionId, { namespace: node.nacosNamespace || "", namespaceName: node.nacosNamespaceName || node.label });
} else if (node.type === "etcd-root" && node.connectionId) {
const tabTitle = `${connectionStore.getConfig(node.connectionId)?.name || "etcd"}:keys`;
queryStore.createTab(node.connectionId, "", tabTitle, "etcd");
@ -1371,6 +1377,15 @@ const showCreateDatabaseDialog = ref(false);
const createDatabaseName = ref("");
const createDatabaseCharset = ref("utf8mb4");
const createDatabaseCollation = ref("utf8mb4_unicode_ci");
const showCreateNacosNamespaceDialog = ref(false);
const createNacosNamespaceId = ref("");
const createNacosNamespaceName = ref("");
const createNacosNamespaceDesc = ref("");
const createNacosNamespaceLoading = ref(false);
const showEditNacosNamespaceDialog = ref(false);
const editNacosNamespaceName = ref("");
const editNacosNamespaceDesc = ref("");
const editNacosNamespaceLoading = ref(false);
const fallbackCreateDatabaseCharset = fallbackCreateDatabaseCharsetMetadata();
const createDatabaseCharsetOptions = ref<string[]>(fallbackCreateDatabaseCharset.charsets);
const createDatabaseCollationsByCharset = ref<Record<string, string[]>>(fallbackCreateDatabaseCharset.collationsByCharset);
@ -1851,6 +1866,17 @@ const canCreateDatabase = computed(() => {
return props.node.type === "connection" && (supportsDatabaseCreation(config?.db_type) || config?.db_type === "duckdb" || (config?.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy"));
});
const canCreateNacosNamespace = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
return props.node.type === "connection" && config?.db_type === "nacos" && !config.read_only;
});
const canEditNacosNamespace = computed(() => {
if (props.node.type !== "nacos-namespace" || !props.node.connectionId || !props.node.nacosNamespace) return false;
const config = connectionStore.getConfig(props.node.connectionId);
return config?.db_type === "nacos" && !config.read_only;
});
const isDuckDbConnection = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
return props.node.type === "connection" && config?.db_type === "duckdb";
@ -2037,6 +2063,63 @@ async function loadCreateDatabaseCharsetMetadata() {
}
}
function openCreateNacosNamespaceDialog() {
createNacosNamespaceId.value = "";
createNacosNamespaceName.value = "";
createNacosNamespaceDesc.value = "";
showCreateNacosNamespaceDialog.value = true;
}
async function confirmCreateNacosNamespace() {
const node = props.node;
const namespaceName = createNacosNamespaceName.value.trim();
if (!node.connectionId || !namespaceName || createNacosNamespaceLoading.value) return;
createNacosNamespaceLoading.value = true;
try {
await api.nacosCreateNamespace(node.connectionId, {
namespaceId: createNacosNamespaceId.value.trim() || undefined,
namespaceName,
namespaceDesc: createNacosNamespaceDesc.value.trim() || namespaceName,
});
showCreateNacosNamespaceDialog.value = false;
await connectionStore.loadNacosNamespaces(node.connectionId, { force: true });
node.isExpanded = true;
toast(t("nacos.namespaceCreated", { name: namespaceName }), 3000);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
} finally {
createNacosNamespaceLoading.value = false;
}
}
function openEditNacosNamespaceDialog() {
editNacosNamespaceName.value = props.node.nacosNamespaceName || props.node.label;
editNacosNamespaceDesc.value = props.node.comment || "";
showEditNacosNamespaceDialog.value = true;
}
async function confirmEditNacosNamespace() {
const node = props.node;
const namespaceId = node.nacosNamespace?.trim() || "";
const namespaceName = editNacosNamespaceName.value.trim();
if (!node.connectionId || !namespaceId || !namespaceName || editNacosNamespaceLoading.value) return;
editNacosNamespaceLoading.value = true;
try {
await api.nacosUpdateNamespace(node.connectionId, {
namespaceId,
namespaceName,
namespaceDesc: editNacosNamespaceDesc.value.trim() || namespaceName,
});
showEditNacosNamespaceDialog.value = false;
await connectionStore.loadNacosNamespaces(node.connectionId, { force: true });
toast(t("nacos.namespaceUpdated", { name: namespaceName }), 3000);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
} finally {
editNacosNamespaceLoading.value = false;
}
}
function ensureDuckDbFileExtension(path: string): string {
return /\.(duckdb|db)$/i.test(path) ? path : `${path}.duckdb`;
}
@ -2861,7 +2944,7 @@ const nodeIconClass = computed(() => {
const canConfigureVisibleDatabases = computed(() => {
if (props.node.type !== "connection" || !props.node.connectionId) return false;
const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type;
return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq";
return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq" && dbType !== "nacos";
});
const canCopyFinalProxyPort = computed(() => {
if (props.node.type !== "connection" || !props.node.connectionId) return false;
@ -3273,6 +3356,13 @@ function treeItemMenuItems(): ContextMenuItem[] {
icon: Plus,
});
}
if (canCreateNacosNamespace.value) {
items.push({
label: t("nacos.createNamespace"),
action: openCreateNacosNamespaceDialog,
icon: FolderPlus,
});
}
items.push({ label: "", separator: true });
if (availableGroups.value.length > 0 || currentGroupId.value) {
const groupChildren: ContextMenuItem[] = availableGroups.value.map((group: { id: string; name: string }) => ({
@ -3455,6 +3545,22 @@ function treeItemMenuItems(): ContextMenuItem[] {
return items;
}
if (node.type === "nacos-namespace") {
items.push({ label: t("contextMenu.openConnection"), action: toggle, icon: FolderOpen });
if (canEditNacosNamespace.value) {
items.push({ label: t("nacos.editNamespace"), action: openEditNacosNamespaceDialog, icon: Pencil });
}
items.push({
label: t("contextMenu.refreshChildren"),
action: refresh,
icon: RefreshCw,
shortcut: shortcutRefresh,
});
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
return items;
}
if (node.type === "mongo-collection") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
@ -4004,6 +4110,58 @@ function treeItemMenuItems(): ContextMenuItem[] {
</DialogContent>
</Dialog>
<Dialog v-model:open="showCreateNacosNamespaceDialog">
<DialogContent class="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle>{{ t("nacos.createNamespace") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3">
<div class="grid gap-1.5">
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceId") }}</label>
<Input v-model="createNacosNamespaceId" :placeholder="t('nacos.namespaceIdPlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
</div>
<div class="grid gap-1.5">
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceName") }}</label>
<Input v-model="createNacosNamespaceName" :placeholder="t('nacos.namespaceNamePlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
</div>
<div class="grid gap-1.5">
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceDesc") }}</label>
<Input v-model="createNacosNamespaceDesc" :placeholder="t('nacos.namespaceDescPlaceholder')" @keydown.enter.prevent="confirmCreateNacosNamespace" />
</div>
</div>
<DialogFooter>
<Button variant="outline" :disabled="createNacosNamespaceLoading" @click="showCreateNacosNamespaceDialog = false">{{ t("dangerDialog.cancel") }}</Button>
<Button :disabled="!createNacosNamespaceName.trim() || createNacosNamespaceLoading" @click="confirmCreateNacosNamespace">
{{ createNacosNamespaceLoading ? t("nacos.creatingNamespace") : t("dangerDialog.confirm") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="showEditNacosNamespaceDialog">
<DialogContent class="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle>{{ t("nacos.editNamespace") }}</DialogTitle>
</DialogHeader>
<div class="grid gap-3">
<div class="grid gap-1.5">
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceName") }}</label>
<Input v-model="editNacosNamespaceName" :placeholder="t('nacos.namespaceNamePlaceholder')" @keydown.enter.prevent="confirmEditNacosNamespace" />
</div>
<div class="grid gap-1.5">
<label class="text-xs font-medium text-muted-foreground">{{ t("nacos.namespaceDesc") }}</label>
<Input v-model="editNacosNamespaceDesc" :placeholder="t('nacos.namespaceDescPlaceholder')" @keydown.enter.prevent="confirmEditNacosNamespace" />
</div>
</div>
<DialogFooter>
<Button variant="outline" :disabled="editNacosNamespaceLoading" @click="showEditNacosNamespaceDialog = false">{{ t("dangerDialog.cancel") }}</Button>
<Button :disabled="!editNacosNamespaceName.trim() || editNacosNamespaceLoading" @click="confirmEditNacosNamespace">
{{ editNacosNamespaceLoading ? t("nacos.updatingNamespace") : t("dangerDialog.confirm") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DangerConfirmDialog
v-model:open="showDropDatabaseConfirm"
:title="t('contextMenu.confirmDropDatabaseTitle')"

View File

@ -49,7 +49,7 @@ const terminalStatus = ref<SqlFileStatus | "idle">("idle");
const terminalError = ref("");
const refreshedTarget = ref(false);
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd"].includes(c.db_type)));
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd", "mq", "nacos"].includes(c.db_type)));
const selectedConnection = computed(() => sqlConnections.value.find((c) => c.id === connectionId.value));

View File

@ -250,6 +250,20 @@ export default {
etcdClientCertBrowse: "Choose client certificate",
etcdClientKeyBrowse: "Choose client private key",
etcdClientCertPairRequired: "Client certificate and private key must be provided together.",
nacosConsoleUrl: "Console URL",
nacosConsoleUrlHint: "Use the Nacos console/admin API address. Nacos 3 Docker usually exposes the console on 8085; older deployments may share 8848 with the service port.",
nacosConsoleUrlRequired: "Nacos Console URL is required",
nacosConsoleUrlAutoAdjusted: "Adjusted Nacos Console URL from {from} to {to}.",
nacosNamespace: "Namespace",
nacosContextPath: "Context Path",
nacosContextPathPlaceholder: "Leave empty or /nacos",
nacosAuth: "Auth",
nacosAuthNone: "None",
nacosAuthUserPassword: "User / Password",
nacosUsernameRequired: "Nacos username is required",
nacosTls: "TLS",
nacosTlsSkipVerify: "Skip certificate verification",
nacosPageSize: "Page Size",
searchDatabasePlaceholder: "Search database types",
iconView: "Icon view",
listView: "List view",
@ -2720,4 +2734,121 @@ export default {
exportError: "Export failed: {error}",
exportCancelled: "Export cancelled",
},
nacos: {
configs: "Configs",
services: "Services",
raw: "Raw",
namespace: "Namespace",
createNamespace: "Create Namespace",
editNamespace: "Edit Namespace",
creatingNamespace: "Creating...",
updatingNamespace: "Updating...",
namespaceCreated: 'Namespace "{name}" created',
namespaceUpdated: 'Namespace "{name}" updated',
namespaceId: "Namespace ID",
namespaceIdPlaceholder: "Leave empty for Nacos to generate",
namespaceName: "Namespace Name",
namespaceNamePlaceholder: "Enter a namespace name",
namespaceDesc: "Description",
namespaceDescPlaceholder: "Defaults to namespace name",
dataId: "Data ID",
group: "Group",
format: "Format",
content: "Content",
tags: "Tags",
application: "Application",
description: "Description",
advanced: "Advanced",
collapse: "Collapse",
load: "Load",
save: "Save",
saveAs: "Save as",
saving: "Saving...",
saved: "Config saved",
createdAndLoaded: "Created and loaded {dataId}",
savedAndLoaded: "Saved and refreshed {dataId}",
deleted: "Config deleted",
delete: "Delete",
copy: "Copy",
copied: "Copied",
export: "Export",
exported: "Export copied",
history: "History",
configHistory: "Config history",
historyDetail: "History detail",
historyCompareTitle: "History Version Compare",
currentPublishedContent: "Current published content:",
historyVersionContent: "History version content:",
updatedAt: "Updated at",
operationType: "Operation",
operator: "Operator",
view: "View",
compare: "Compare",
rollback: "Rollback",
loadingHistory: "Loading history...",
noHistory: "No history versions",
noHistoryHint: "Refresh if this config was just published. Some Nacos versions may not expose history APIs.",
historyCreate: "Create",
historyUpdate: "Update",
historyDelete: "Delete",
historyRollback: "Rollback",
confirmRollbackTitle: "Rollback Nacos config",
confirmRollbackMessage: "This will roll the current config back to the selected history version.",
rollbackSuccess: "Config rolled back",
publish: "Publish",
dataIdRequired: "Data ID is required",
allGroups: "All groups",
noConfigs: "No configs",
selectConfig: "Select or create a config",
noServices: "No services",
selectService: "Select a service",
service: "Service",
instances: "Instances",
instanceCount: "{count} instances",
refresh: "Refresh",
address: "Address",
cluster: "Cluster",
weight: "Weight",
state: "State",
actions: "Actions",
query: "Query",
body: "Body",
response: "Response",
template: "Template",
configDiffTitle: "Config Content Compare",
inlineCompare: "Inline compare",
currentVersionContent: "Current official version:",
publishVersionContent: "This release content:",
confirmSaveTitle: "Review Nacos config changes",
confirmSaveMessage: "Save this config change? +{added} / -{removed} changed lines.",
confirmDeleteTitle: "Delete Nacos config",
confirmDeleteMessage: "This removes the selected config from Nacos.",
confirmInstanceTitle: "Update Nacos instance",
confirmInstanceMessage: "Confirm the target service instance state.",
confirmRawTitle: "Send mutating Raw API request",
confirmRawMessage: "This Raw API request can modify Nacos state.",
rawTemplate: {
serverState: "Server state",
namespaceList: "Namespace list",
configDetail: "Config detail",
serviceList: "Service list",
instanceList: "Instance list",
},
healthy: "Healthy",
unhealthy: "Unhealthy",
enabled: "Enabled",
offline: "Offline",
ephemeral: "Ephemeral",
persistent: "Persistent",
enable: "Enable",
disable: "Disable",
markHealthy: "Healthy",
markUnhealthy: "Unhealthy",
send: "Send",
total: "{count} total",
prev: "Prev",
next: "Next",
test: "Test",
readOnly: "Read only",
},
};

View File

@ -230,6 +230,20 @@ export default {
redisSentinelTls: "TLS Sentinel",
redisSentinelTlsHint: "Usar TLS al conectar con nodos Sentinel",
redisKeySeparator: "Separador de espacio de nombres de clave",
nacosConsoleUrl: "URL de consola",
nacosConsoleUrlHint: "Usa la dirección de consola/admin API de Nacos. Nacos 3 Docker normalmente expone la consola en 8085; despliegues antiguos pueden compartir 8848 con el puerto de servicio.",
nacosConsoleUrlRequired: "La URL de consola de Nacos es obligatoria",
nacosConsoleUrlAutoAdjusted: "URL de consola de Nacos ajustada de {from} a {to}.",
nacosNamespace: "Namespace",
nacosContextPath: "Context Path",
nacosContextPathPlaceholder: "Dejar vacío o /nacos",
nacosAuth: "Autenticación",
nacosAuthNone: "Ninguna",
nacosAuthUserPassword: "Usuario / Contraseña",
nacosUsernameRequired: "El usuario de Nacos es obligatorio",
nacosTls: "TLS",
nacosTlsSkipVerify: "Omitir verificación de certificado",
nacosPageSize: "Tamaño de página",
searchDatabasePlaceholder: "Buscar tipos de bases de datos",
iconView: "Vista de íconos",
listView: "Vista de lista",
@ -2359,4 +2373,109 @@ export default {
exportError: "Error al exportar: {error}",
exportCancelled: "Exportación cancelada",
},
nacos: {
configs: "Configuraciones",
services: "Servicios",
raw: "Raw",
namespace: "Namespace",
dataId: "Data ID",
group: "Grupo",
format: "Formato",
content: "Contenido",
tags: "Etiquetas",
application: "Aplicación",
description: "Descripción",
advanced: "Avanzado",
collapse: "Contraer",
load: "Cargar",
save: "Guardar",
saveAs: "Guardar como",
saving: "Guardando...",
saved: "Configuración guardada",
createdAndLoaded: "{dataId} creado y cargado",
savedAndLoaded: "{dataId} guardado y actualizado",
deleted: "Configuración eliminada",
delete: "Eliminar",
copy: "Copiar",
copied: "Copiado",
export: "Exportar",
exported: "Exportación copiada",
history: "History",
configHistory: "Config history",
historyDetail: "History detail",
historyCompareTitle: "History Version Compare",
currentPublishedContent: "Current published content:",
historyVersionContent: "History version content:",
updatedAt: "Updated at",
operationType: "Operation",
operator: "Operator",
view: "View",
compare: "Compare",
rollback: "Rollback",
loadingHistory: "Loading history...",
noHistory: "No history versions",
noHistoryHint: "Refresh if this config was just published. Some Nacos versions may not expose history APIs.",
historyCreate: "Create",
historyUpdate: "Update",
historyDelete: "Delete",
historyRollback: "Rollback",
confirmRollbackTitle: "Rollback Nacos config",
confirmRollbackMessage: "This will roll the current config back to the selected history version.",
rollbackSuccess: "Config rolled back",
publish: "Publicar",
dataIdRequired: "Data ID es obligatorio",
allGroups: "Todos los grupos",
noConfigs: "Sin configuraciones",
selectConfig: "Selecciona o crea una configuración",
noServices: "Sin servicios",
selectService: "Selecciona un servicio",
service: "Servicio",
instances: "Instancias",
instanceCount: "{count} instancias",
refresh: "Actualizar",
address: "Dirección",
cluster: "Clúster",
weight: "Peso",
state: "Estado",
actions: "Acciones",
query: "Consulta",
body: "Cuerpo",
response: "Respuesta",
template: "Plantilla",
configDiffTitle: "Comparar contenido de configuración",
inlineCompare: "Comparación en línea",
currentVersionContent: "Versión oficial actual:",
publishVersionContent: "Contenido de esta publicación:",
confirmSaveTitle: "Revisar cambios de configuración Nacos",
confirmSaveMessage: "¿Guardar este cambio? +{added} / -{removed} líneas.",
confirmDeleteTitle: "Eliminar configuración Nacos",
confirmDeleteMessage: "Esto elimina la configuración seleccionada de Nacos.",
confirmInstanceTitle: "Actualizar instancia Nacos",
confirmInstanceMessage: "Confirma el estado objetivo de la instancia.",
confirmRawTitle: "Enviar solicitud Raw API modificadora",
confirmRawMessage: "Esta solicitud Raw API puede modificar el estado de Nacos.",
rawTemplate: {
serverState: "Estado del servidor",
namespaceList: "Lista de namespaces",
configDetail: "Detalle de configuración",
serviceList: "Lista de servicios",
instanceList: "Lista de instancias",
},
healthy: "Saludable",
unhealthy: "No saludable",
enabled: "Habilitado",
offline: "Offline",
ephemeral: "Efímera",
persistent: "Persistente",
enable: "Habilitar",
disable: "Deshabilitar",
markHealthy: "Marcar saludable",
markUnhealthy: "Marcar no saludable",
send: "Enviar",
total: "{count} en total",
prev: "Anterior",
next: "Siguiente",
test: "Probar",
readOnly: "Solo lectura",
},
};

View File

@ -235,6 +235,20 @@ export default {
redisSentinelTls: "TLS Sentinel",
redisSentinelTlsHint: "Usa TLS quando ti connetti ai nodi Sentinel",
redisKeySeparator: "Separatore namespace chiavi",
nacosConsoleUrl: "URL console",
nacosConsoleUrlHint: "Usa l'indirizzo console/admin API di Nacos. Nacos 3 Docker di solito espone la console su 8085; distribuzioni più vecchie possono condividere 8848 con la porta di servizio.",
nacosConsoleUrlRequired: "L'URL console Nacos è obbligatorio",
nacosConsoleUrlAutoAdjusted: "URL console Nacos regolato da {from} a {to}.",
nacosNamespace: "Namespace",
nacosContextPath: "Context Path",
nacosContextPathPlaceholder: "Lascia vuoto o /nacos",
nacosAuth: "Autenticazione",
nacosAuthNone: "Nessuna",
nacosAuthUserPassword: "Utente / Password",
nacosUsernameRequired: "Il nome utente Nacos è obbligatorio",
nacosTls: "TLS",
nacosTlsSkipVerify: "Salta verifica certificato",
nacosPageSize: "Dimensione pagina",
searchDatabasePlaceholder: "Cerca tipi di database",
iconView: "Vista icone",
listView: "Vista elenco",
@ -2428,4 +2442,109 @@ export default {
exportError: "Esportazione non riuscita: {error}",
exportCancelled: "Esportazione annullata",
},
nacos: {
configs: "Configurazioni",
services: "Servizi",
raw: "Raw",
namespace: "Namespace",
dataId: "Data ID",
group: "Gruppo",
format: "Formato",
content: "Contenuto",
tags: "Tag",
application: "Applicazione",
description: "Descrizione",
advanced: "Avanzate",
collapse: "Comprimi",
load: "Carica",
save: "Salva",
saveAs: "Salva come",
saving: "Salvataggio...",
saved: "Configurazione salvata",
createdAndLoaded: "{dataId} creato e caricato",
savedAndLoaded: "{dataId} salvato e aggiornato",
deleted: "Configurazione eliminata",
delete: "Elimina",
copy: "Copia",
copied: "Copiato",
export: "Esporta",
exported: "Esportazione copiata",
history: "History",
configHistory: "Config history",
historyDetail: "History detail",
historyCompareTitle: "History Version Compare",
currentPublishedContent: "Current published content:",
historyVersionContent: "History version content:",
updatedAt: "Updated at",
operationType: "Operation",
operator: "Operator",
view: "View",
compare: "Compare",
rollback: "Rollback",
loadingHistory: "Loading history...",
noHistory: "No history versions",
noHistoryHint: "Refresh if this config was just published. Some Nacos versions may not expose history APIs.",
historyCreate: "Create",
historyUpdate: "Update",
historyDelete: "Delete",
historyRollback: "Rollback",
confirmRollbackTitle: "Rollback Nacos config",
confirmRollbackMessage: "This will roll the current config back to the selected history version.",
rollbackSuccess: "Config rolled back",
publish: "Pubblica",
dataIdRequired: "Data ID è obbligatorio",
allGroups: "Tutti i gruppi",
noConfigs: "Nessuna configurazione",
selectConfig: "Seleziona o crea una configurazione",
noServices: "Nessun servizio",
selectService: "Seleziona un servizio",
service: "Servizio",
instances: "Istanze",
instanceCount: "{count} istanze",
refresh: "Aggiorna",
address: "Indirizzo",
cluster: "Cluster",
weight: "Peso",
state: "Stato",
actions: "Azioni",
query: "Query",
body: "Corpo",
response: "Risposta",
template: "Template",
configDiffTitle: "Confronto contenuto configurazione",
inlineCompare: "Confronto inline",
currentVersionContent: "Versione ufficiale attuale:",
publishVersionContent: "Contenuto di questa pubblicazione:",
confirmSaveTitle: "Rivedi modifiche configurazione Nacos",
confirmSaveMessage: "Salvare questa modifica? +{added} / -{removed} righe.",
confirmDeleteTitle: "Elimina configurazione Nacos",
confirmDeleteMessage: "Questo rimuove la configurazione selezionata da Nacos.",
confirmInstanceTitle: "Aggiorna istanza Nacos",
confirmInstanceMessage: "Conferma lo stato di destinazione dell'istanza.",
confirmRawTitle: "Invia richiesta Raw API modificante",
confirmRawMessage: "Questa richiesta Raw API può modificare lo stato di Nacos.",
rawTemplate: {
serverState: "Stato server",
namespaceList: "Lista namespace",
configDetail: "Dettaglio configurazione",
serviceList: "Lista servizi",
instanceList: "Lista istanze",
},
healthy: "Sana",
unhealthy: "Non sana",
enabled: "Abilitata",
offline: "Offline",
ephemeral: "Effimera",
persistent: "Persistente",
enable: "Abilita",
disable: "Disabilita",
markHealthy: "Segna sana",
markUnhealthy: "Segna non sana",
send: "Invia",
total: "{count} totali",
prev: "Prec.",
next: "Succ.",
test: "Test",
readOnly: "Sola lettura",
},
};

View File

@ -249,6 +249,20 @@ export default {
etcdClientCertBrowse: "クライアント証明書を選択",
etcdClientKeyBrowse: "クライアント秘密鍵を選択",
etcdClientCertPairRequired: "クライアント証明書と秘密鍵の両方を指定する必要があります。",
nacosConsoleUrl: "コンソールURL",
nacosConsoleUrlHint: "Nacosのコンソール/admin APIアドレスを指定します。Nacos 3 Dockerでは通常8085、古い構成ではサービス用ポート8848と共用される場合があります。",
nacosConsoleUrlRequired: "NacosコンソールURLは必須です",
nacosConsoleUrlAutoAdjusted: "NacosコンソールURLを{from}から{to}に自動調整しました。",
nacosNamespace: "名前空間",
nacosContextPath: "Context Path",
nacosContextPathPlaceholder: "空欄または /nacos",
nacosAuth: "認証",
nacosAuthNone: "なし",
nacosAuthUserPassword: "ユーザー / パスワード",
nacosUsernameRequired: "Nacosユーザー名は必須です",
nacosTls: "TLS",
nacosTlsSkipVerify: "証明書検証をスキップ",
nacosPageSize: "ページサイズ",
searchDatabasePlaceholder: "データベースタイプを検索",
iconView: "アイコン表示",
listView: "リスト表示",
@ -2674,4 +2688,109 @@ export default {
exportError: "エクスポートに失敗しました: {error}",
exportCancelled: "エクスポートがキャンセルされました",
},
nacos: {
configs: "設定",
services: "サービス",
raw: "Raw",
namespace: "名前空間",
dataId: "Data ID",
group: "Group",
format: "設定形式",
content: "設定内容",
tags: "タグ",
application: "アプリケーション",
description: "説明",
advanced: "詳細設定",
collapse: "閉じる",
load: "読み込み",
save: "保存",
saveAs: "別名で保存",
saving: "保存中...",
saved: "設定を保存しました",
createdAndLoaded: "{dataId}を作成して読み込みました",
savedAndLoaded: "{dataId}を保存して更新しました",
deleted: "設定を削除しました",
delete: "削除",
copy: "コピー",
copied: "コピーしました",
export: "エクスポート",
exported: "エクスポート内容をコピーしました",
history: "History",
configHistory: "Config history",
historyDetail: "History detail",
historyCompareTitle: "History Version Compare",
currentPublishedContent: "Current published content:",
historyVersionContent: "History version content:",
updatedAt: "Updated at",
operationType: "Operation",
operator: "Operator",
view: "View",
compare: "Compare",
rollback: "Rollback",
loadingHistory: "Loading history...",
noHistory: "No history versions",
noHistoryHint: "Refresh if this config was just published. Some Nacos versions may not expose history APIs.",
historyCreate: "Create",
historyUpdate: "Update",
historyDelete: "Delete",
historyRollback: "Rollback",
confirmRollbackTitle: "Rollback Nacos config",
confirmRollbackMessage: "This will roll the current config back to the selected history version.",
rollbackSuccess: "Config rolled back",
publish: "公開",
dataIdRequired: "Data ID は必須です",
allGroups: "すべてのグループ",
noConfigs: "設定がありません",
selectConfig: "設定を選択または作成",
noServices: "サービスがありません",
selectService: "サービスを選択",
service: "サービス",
instances: "インスタンス",
instanceCount: "{count}件のインスタンス",
refresh: "更新",
address: "アドレス",
cluster: "クラスター",
weight: "重み",
state: "状態",
actions: "操作",
query: "クエリ",
body: "本文",
response: "レスポンス",
template: "テンプレート",
configDiffTitle: "設定内容の比較",
inlineCompare: "行内比較",
currentVersionContent: "現在の正式バージョン:",
publishVersionContent: "今回の公開内容:",
confirmSaveTitle: "Nacos設定の変更を確認",
confirmSaveMessage: "この設定変更を保存しますか? +{added} / -{removed} 行。",
confirmDeleteTitle: "Nacos設定を削除",
confirmDeleteMessage: "選択した設定をNacosから削除します。",
confirmInstanceTitle: "Nacosインスタンスを更新",
confirmInstanceMessage: "対象サービスインスタンスの状態を確認してください。",
confirmRawTitle: "変更系Raw APIリクエストを送信",
confirmRawMessage: "このRaw APIリクエストはNacos状態を変更する可能性があります。",
rawTemplate: {
serverState: "サーバー状態",
namespaceList: "名前空間一覧",
configDetail: "設定詳細",
serviceList: "サービス一覧",
instanceList: "インスタンス一覧",
},
healthy: "正常",
unhealthy: "異常",
enabled: "有効",
offline: "オフライン",
ephemeral: "一時",
persistent: "永続",
enable: "有効化",
disable: "無効化",
markHealthy: "正常にする",
markUnhealthy: "異常にする",
send: "送信",
total: "合計 {count} 件",
prev: "前へ",
next: "次へ",
test: "テスト",
readOnly: "読み取り専用",
},
};

View File

@ -235,6 +235,20 @@ export default {
redisSentinelTls: "TLS do Sentinel",
redisSentinelTlsHint: "Usar TLS ao conectar aos nós Sentinel",
redisKeySeparator: "Separador de namespace de chave",
nacosConsoleUrl: "URL do console",
nacosConsoleUrlHint: "Use o endereço do console/admin API do Nacos. O Nacos 3 Docker geralmente expõe o console em 8085; implantações antigas podem compartilhar 8848 com a porta de serviço.",
nacosConsoleUrlRequired: "A URL do console Nacos é obrigatória",
nacosConsoleUrlAutoAdjusted: "URL do console Nacos ajustada de {from} para {to}.",
nacosNamespace: "Namespace",
nacosContextPath: "Context Path",
nacosContextPathPlaceholder: "Deixe vazio ou /nacos",
nacosAuth: "Autenticação",
nacosAuthNone: "Nenhuma",
nacosAuthUserPassword: "Usuário / Senha",
nacosUsernameRequired: "O usuário Nacos é obrigatório",
nacosTls: "TLS",
nacosTlsSkipVerify: "Ignorar verificação do certificado",
nacosPageSize: "Tamanho da página",
searchDatabasePlaceholder: "Pesquisar tipos de banco de dados",
iconView: "Visualização em ícones",
listView: "Visualização em lista",
@ -2439,4 +2453,109 @@ export default {
exportError: "Falha na exportação: {error}",
exportCancelled: "Exportação cancelada",
},
nacos: {
configs: "Configurações",
services: "Serviços",
raw: "Raw",
namespace: "Namespace",
dataId: "Data ID",
group: "Grupo",
format: "Formato",
content: "Conteúdo",
tags: "Tags",
application: "Aplicação",
description: "Descrição",
advanced: "Avançado",
collapse: "Recolher",
load: "Carregar",
save: "Salvar",
saveAs: "Salvar como",
saving: "Salvando...",
saved: "Configuração salva",
createdAndLoaded: "{dataId} criado e carregado",
savedAndLoaded: "{dataId} salvo e atualizado",
deleted: "Configuração excluída",
delete: "Excluir",
copy: "Copiar",
copied: "Copiado",
export: "Exportar",
exported: "Exportação copiada",
history: "History",
configHistory: "Config history",
historyDetail: "History detail",
historyCompareTitle: "History Version Compare",
currentPublishedContent: "Current published content:",
historyVersionContent: "History version content:",
updatedAt: "Updated at",
operationType: "Operation",
operator: "Operator",
view: "View",
compare: "Compare",
rollback: "Rollback",
loadingHistory: "Loading history...",
noHistory: "No history versions",
noHistoryHint: "Refresh if this config was just published. Some Nacos versions may not expose history APIs.",
historyCreate: "Create",
historyUpdate: "Update",
historyDelete: "Delete",
historyRollback: "Rollback",
confirmRollbackTitle: "Rollback Nacos config",
confirmRollbackMessage: "This will roll the current config back to the selected history version.",
rollbackSuccess: "Config rolled back",
publish: "Publicar",
dataIdRequired: "Data ID é obrigatório",
allGroups: "Todos os grupos",
noConfigs: "Nenhuma configuração",
selectConfig: "Selecione ou crie uma configuração",
noServices: "Nenhum serviço",
selectService: "Selecione um serviço",
service: "Serviço",
instances: "Instâncias",
instanceCount: "{count} instâncias",
refresh: "Atualizar",
address: "Endereço",
cluster: "Cluster",
weight: "Peso",
state: "Estado",
actions: "Ações",
query: "Query",
body: "Corpo",
response: "Resposta",
template: "Modelo",
configDiffTitle: "Comparar conteúdo da configuração",
inlineCompare: "Comparação inline",
currentVersionContent: "Versão oficial atual:",
publishVersionContent: "Conteúdo desta publicação:",
confirmSaveTitle: "Revisar alterações da configuração Nacos",
confirmSaveMessage: "Salvar esta alteração? +{added} / -{removed} linhas.",
confirmDeleteTitle: "Excluir configuração Nacos",
confirmDeleteMessage: "Isso remove a configuração selecionada do Nacos.",
confirmInstanceTitle: "Atualizar instância Nacos",
confirmInstanceMessage: "Confirme o estado de destino da instância.",
confirmRawTitle: "Enviar solicitação Raw API mutável",
confirmRawMessage: "Esta solicitação Raw API pode modificar o estado do Nacos.",
rawTemplate: {
serverState: "Estado do servidor",
namespaceList: "Lista de namespaces",
configDetail: "Detalhe da configuração",
serviceList: "Lista de serviços",
instanceList: "Lista de instâncias",
},
healthy: "Saudável",
unhealthy: "Não saudável",
enabled: "Ativado",
offline: "Offline",
ephemeral: "Efêmera",
persistent: "Persistente",
enable: "Ativar",
disable: "Desativar",
markHealthy: "Marcar saudável",
markUnhealthy: "Marcar não saudável",
send: "Enviar",
total: "{count} no total",
prev: "Anterior",
next: "Próximo",
test: "Testar",
readOnly: "Somente leitura",
},
};

View File

@ -251,6 +251,20 @@ export default {
etcdClientCertBrowse: "选择客户端证书",
etcdClientKeyBrowse: "选择客户端私钥",
etcdClientCertPairRequired: "客户端证书和私钥必须一起填写。",
nacosConsoleUrl: "控制台 URL",
nacosConsoleUrlHint: "填写 Nacos 控制台/admin API 地址。Nacos 3 Docker 通常是 8085如果旧版控制台和服务共用 8848也可以填 8848。",
nacosConsoleUrlRequired: "Nacos 控制台 URL 不能为空",
nacosConsoleUrlAutoAdjusted: "已自动将 Nacos 控制台 URL 从 {from} 调整为 {to}。",
nacosNamespace: "命名空间",
nacosContextPath: "Context Path",
nacosContextPathPlaceholder: "留空或填写 /nacos",
nacosAuth: "认证",
nacosAuthNone: "无",
nacosAuthUserPassword: "用户名 / 密码",
nacosUsernameRequired: "Nacos 用户名不能为空",
nacosTls: "TLS",
nacosTlsSkipVerify: "跳过证书验证",
nacosPageSize: "分页大小",
searchDatabasePlaceholder: "搜索数据库类型",
iconView: "图标视图",
listView: "列表视图",
@ -2744,4 +2758,121 @@ export default {
exportError: "导出失败: {error}",
exportCancelled: "导出已取消",
},
nacos: {
configs: "配置",
services: "服务",
raw: "Raw",
namespace: "命名空间",
createNamespace: "新建命名空间",
editNamespace: "编辑命名空间",
creatingNamespace: "正在创建...",
updatingNamespace: "正在更新...",
namespaceCreated: "命名空间「{name}」已创建",
namespaceUpdated: "命名空间「{name}」已更新",
namespaceId: "命名空间 ID",
namespaceIdPlaceholder: "留空则由 Nacos 自动生成",
namespaceName: "命名空间名",
namespaceNamePlaceholder: "请输入命名空间名",
namespaceDesc: "描述",
namespaceDescPlaceholder: "留空则使用命名空间名",
dataId: "Data ID",
group: "Group",
format: "配置格式",
content: "配置内容",
tags: "标签",
application: "归属应用",
description: "描述",
advanced: "高级配置",
collapse: "收起",
load: "加载",
save: "保存",
saveAs: "另存为",
saving: "保存中...",
saved: "配置已保存",
createdAndLoaded: "已创建并加载 {dataId}",
savedAndLoaded: "已保存并刷新 {dataId}",
deleted: "配置已删除",
delete: "删除",
copy: "复制",
copied: "已复制",
export: "导出",
exported: "导出内容已复制",
history: "历史",
configHistory: "配置历史",
historyDetail: "历史版本详情",
historyCompareTitle: "历史版本对比",
currentPublishedContent: "当前已发布内容:",
historyVersionContent: "历史版本内容:",
updatedAt: "更新时间",
operationType: "操作类型",
operator: "操作人",
view: "查看",
compare: "对比",
rollback: "回滚",
loadingHistory: "正在加载历史版本...",
noHistory: "暂无历史版本",
noHistoryHint: "如果刚发布过配置,请刷新;部分 Nacos 版本可能未开启历史记录接口。",
historyCreate: "新增",
historyUpdate: "更新",
historyDelete: "删除",
historyRollback: "回滚",
confirmRollbackTitle: "回滚 Nacos 配置",
confirmRollbackMessage: "该操作会将当前配置回滚到所选历史版本。",
rollbackSuccess: "配置已回滚",
publish: "发布",
dataIdRequired: "Data ID 不能为空",
allGroups: "全部分组",
noConfigs: "暂无配置",
selectConfig: "选择或新建配置",
noServices: "暂无服务",
selectService: "选择服务",
service: "服务",
instances: "实例",
instanceCount: "{count} 个实例",
refresh: "刷新",
address: "地址",
cluster: "集群",
weight: "权重",
state: "状态",
actions: "操作",
query: "查询参数",
body: "请求体",
response: "响应",
template: "模板",
configDiffTitle: "配置内容对比",
inlineCompare: "行内对比",
currentVersionContent: "当前正式版本内容:",
publishVersionContent: "本次发布内容:",
confirmSaveTitle: "确认保存 Nacos 配置",
confirmSaveMessage: "是否保存这次配置变更?新增 {added} 行,删除 {removed} 行。",
confirmDeleteTitle: "删除 Nacos 配置",
confirmDeleteMessage: "该操作会从 Nacos 删除所选配置。",
confirmInstanceTitle: "更新 Nacos 实例",
confirmInstanceMessage: "请确认目标服务实例状态。",
confirmRawTitle: "发送变更型 Raw API 请求",
confirmRawMessage: "该 Raw API 请求可能会修改 Nacos 状态。",
rawTemplate: {
serverState: "服务状态",
namespaceList: "命名空间列表",
configDetail: "配置详情",
serviceList: "服务列表",
instanceList: "实例列表",
},
healthy: "健康",
unhealthy: "不健康",
enabled: "已启用",
offline: "已下线",
ephemeral: "临时实例",
persistent: "持久实例",
enable: "启用",
disable: "禁用",
markHealthy: "标记健康",
markUnhealthy: "标记不健康",
send: "发送",
total: "共 {count} 条",
prev: "上一页",
next: "下一页",
test: "测试",
readOnly: "只读",
},
};

View File

@ -235,6 +235,20 @@ export default {
redisSentinelTls: "Sentinel TLS",
redisSentinelTlsHint: "連線 Sentinel 節點時使用 TLS",
redisKeySeparator: "鍵名分隔符(可選)",
nacosConsoleUrl: "控制台 URL",
nacosConsoleUrlHint: "填寫 Nacos 控制台/admin API 位址。Nacos 3 Docker 通常是 8085如果舊版控制台和服務共用 8848也可以填 8848。",
nacosConsoleUrlRequired: "Nacos 控制台 URL 不能為空",
nacosConsoleUrlAutoAdjusted: "已自動將 Nacos 控制台 URL 從 {from} 調整為 {to}。",
nacosNamespace: "命名空間",
nacosContextPath: "Context Path",
nacosContextPathPlaceholder: "留空或填寫 /nacos",
nacosAuth: "認證",
nacosAuthNone: "無",
nacosAuthUserPassword: "使用者名稱 / 密碼",
nacosUsernameRequired: "Nacos 使用者名稱不能為空",
nacosTls: "TLS",
nacosTlsSkipVerify: "跳過憑證驗證",
nacosPageSize: "分頁大小",
searchDatabasePlaceholder: "搜尋資料庫類型",
iconView: "圖示檢視",
listView: "清單檢視",
@ -2453,4 +2467,109 @@ export default {
exportError: "匯出失敗: {error}",
exportCancelled: "匯出已取消",
},
nacos: {
configs: "配置",
services: "服務",
raw: "Raw",
namespace: "命名空間",
dataId: "Data ID",
group: "Group",
format: "配置格式",
content: "配置內容",
tags: "標籤",
application: "歸屬應用",
description: "描述",
advanced: "進階配置",
collapse: "收起",
load: "載入",
save: "儲存",
saveAs: "另存為",
saving: "儲存中...",
saved: "配置已儲存",
createdAndLoaded: "已建立並載入 {dataId}",
savedAndLoaded: "已儲存並重新整理 {dataId}",
deleted: "配置已刪除",
delete: "刪除",
copy: "複製",
copied: "已複製",
export: "匯出",
exported: "匯出內容已複製",
history: "歷史",
configHistory: "配置歷史",
historyDetail: "歷史版本詳情",
historyCompareTitle: "歷史版本對比",
currentPublishedContent: "目前已發布內容:",
historyVersionContent: "歷史版本內容:",
updatedAt: "更新時間",
operationType: "操作類型",
operator: "操作人",
view: "查看",
compare: "對比",
rollback: "回滾",
loadingHistory: "正在載入歷史版本...",
noHistory: "暫無歷史版本",
noHistoryHint: "如果剛發布過配置,請重新整理;部分 Nacos 版本可能未開啟歷史記錄介面。",
historyCreate: "新增",
historyUpdate: "更新",
historyDelete: "刪除",
historyRollback: "回滾",
confirmRollbackTitle: "回滾 Nacos 配置",
confirmRollbackMessage: "該操作會將目前配置回滾到所選歷史版本。",
rollbackSuccess: "配置已回滾",
publish: "發布",
dataIdRequired: "Data ID 不能為空",
allGroups: "全部分組",
noConfigs: "暫無配置",
selectConfig: "選擇或新增配置",
noServices: "暫無服務",
selectService: "選擇服務",
service: "服務",
instances: "實例",
instanceCount: "{count} 個實例",
refresh: "重新整理",
address: "位址",
cluster: "叢集",
weight: "權重",
state: "狀態",
actions: "操作",
query: "查詢參數",
body: "請求體",
response: "回應",
template: "模板",
configDiffTitle: "配置內容對比",
inlineCompare: "行內對比",
currentVersionContent: "目前正式版本內容:",
publishVersionContent: "本次發布內容:",
confirmSaveTitle: "確認儲存 Nacos 配置",
confirmSaveMessage: "是否儲存這次配置變更?新增 {added} 行,刪除 {removed} 行。",
confirmDeleteTitle: "刪除 Nacos 配置",
confirmDeleteMessage: "此操作會從 Nacos 刪除所選配置。",
confirmInstanceTitle: "更新 Nacos 實例",
confirmInstanceMessage: "請確認目標服務實例狀態。",
confirmRawTitle: "傳送變更型 Raw API 請求",
confirmRawMessage: "此 Raw API 請求可能會修改 Nacos 狀態。",
rawTemplate: {
serverState: "服務狀態",
namespaceList: "命名空間列表",
configDetail: "配置詳情",
serviceList: "服務列表",
instanceList: "實例列表",
},
healthy: "健康",
unhealthy: "不健康",
enabled: "已啟用",
offline: "已下線",
ephemeral: "臨時實例",
persistent: "持久實例",
enable: "啟用",
disable: "停用",
markHealthy: "標記健康",
markUnhealthy: "標記不健康",
send: "傳送",
total: "共 {count} 筆",
prev: "上一頁",
next: "下一頁",
test: "測試",
readOnly: "唯讀",
},
};

View File

@ -24,6 +24,10 @@ describe("quickConnectionOpenTarget", () => {
expect(quickConnectionOpenTarget(connection("mq"))).toEqual({ kind: "mq-admin" });
});
it("opens Nacos connections in the Nacos admin console", () => {
expect(quickConnectionOpenTarget(connection("nacos"))).toEqual({ kind: "nacos-admin" });
});
it("opens regular connections in a query tab", () => {
expect(quickConnectionOpenTarget({ ...connection("postgresql"), database: "app" })).toEqual({
kind: "query",

View File

@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { buildNacosConfigDeleteConfirm, buildNacosInlineDiff, buildNacosInstanceConfirm, buildNacosRawRequest, buildNacosSideBySideDiff, isNacosRawMutation, parseNacosRawBody, parseNacosRawQuery, summarizeNacosConfigDiff } from "../nacosAdmin";
describe("nacosAdmin helpers", () => {
it("parses raw query and body text", () => {
expect(parseNacosRawQuery("?dataId=a&group=DEFAULT_GROUP")).toEqual({ dataId: "a", group: "DEFAULT_GROUP" });
expect(parseNacosRawQuery("")).toBeUndefined();
expect(parseNacosRawBody('{"enabled":false}')).toEqual({ enabled: false });
expect(parseNacosRawBody("plain text")).toBe("plain text");
});
it("builds raw requests and detects mutations", () => {
const req = buildNacosRawRequest("post", " /v1/cs/configs ", "a=1", '{"b":2}');
expect(req).toEqual({ method: "post", path: "/v1/cs/configs", query: { a: "1" }, body: { b: 2 } });
expect(isNacosRawMutation("GET")).toBe(false);
expect(isNacosRawMutation("DELETE")).toBe(true);
});
it("summarizes config diffs", () => {
const diff = summarizeNacosConfigDiff("a\nb", "a\nc\nd");
expect(diff.changed).toBe(true);
expect(diff.removedLines).toBe(1);
expect(diff.addedLines).toBe(2);
expect(diff.preview).toContain("- b");
expect(diff.preview).toContain("+ c");
});
it("builds side-by-side config diff rows with inline segments", () => {
const rows = buildNacosSideBySideDiff('cloud:\n secret: "aaa"\n', 'cloud:\n secret: "aaa1"\n enabled: true\n');
expect(rows[0]).toMatchObject({ leftLineNumber: 1, rightLineNumber: 1, leftType: "equal", rightType: "equal" });
expect(rows[1]).toMatchObject({ leftLineNumber: 2, rightLineNumber: 2, leftType: "modify", rightType: "modify" });
expect(rows[1].rightInline.some((segment) => segment.changed && segment.value === "1")).toBe(true);
expect(rows[2]).toMatchObject({ leftLineNumber: null, rightLineNumber: 3, leftType: "padding", rightType: "insert" });
});
it("builds inline config diff rows with character-level changed segments", () => {
const rows = buildNacosInlineDiff('secretId: "aaa1"\n', 'secretId: "aaa2"\n');
expect(rows).toHaveLength(2);
expect(rows[0]).toMatchObject({ lineNumber: 1, type: "delete" });
expect(rows[1]).toMatchObject({ lineNumber: 1, type: "insert" });
expect(rows[0].segments.some((segment) => segment.changed && segment.value === "1")).toBe(true);
expect(rows[1].segments.some((segment) => segment.changed && segment.value === "2")).toBe(true);
});
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");
expect(details).toContain("serviceName=DEFAULT_GROUP@@svc");
expect(details).toContain("targetEnabled=false");
});
});

View File

@ -228,6 +228,23 @@ export const pendingOpenDbFiles = forward("pendingOpenDbFiles");
export const pendingOpenConnectionLinks = forward("pendingOpenConnectionLinks");
export const readExternalSqlFile = forward("readExternalSqlFile");
// Nacos
export const nacosTestConnection = forward("nacosTestConnection");
export const nacosListNamespaces = forward("nacosListNamespaces");
export const nacosCreateNamespace = forward("nacosCreateNamespace");
export const nacosUpdateNamespace = forward("nacosUpdateNamespace");
export const nacosListConfigs = forward("nacosListConfigs");
export const nacosGetConfig = forward("nacosGetConfig");
export const nacosPublishConfig = forward("nacosPublishConfig");
export const nacosDeleteConfig = forward("nacosDeleteConfig");
export const nacosListConfigHistory = forward("nacosListConfigHistory");
export const nacosGetConfigHistory = forward("nacosGetConfigHistory");
export const nacosRollbackConfig = forward("nacosRollbackConfig");
export const nacosListServices = forward("nacosListServices");
export const nacosListInstances = forward("nacosListInstances");
export const nacosUpdateInstance = forward("nacosUpdateInstance");
export const nacosRawRequest = forward("nacosRawRequest");
// Data Transfer
export const startTransfer = forward("startTransfer");
export const cancelTransfer = forward("cancelTransfer");

View File

@ -1,11 +1,14 @@
import type { ConnectionConfig } from "@/types/database";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
export type QuickConnectionOpenTarget = { kind: "mq-admin" } | { kind: "query"; database: string };
export type QuickConnectionOpenTarget = { kind: "mq-admin" } | { kind: "nacos-admin" } | { kind: "query"; database: string };
export function quickConnectionOpenTarget(connection: Pick<ConnectionConfig, "db_type" | "database">, databaseOptions: string[] = []): QuickConnectionOpenTarget {
if (connection.db_type === "mq") {
return { kind: "mq-admin" };
}
if (connection.db_type === "nacos") {
return { kind: "nacos-admin" };
}
return { kind: "query", database: resolveDefaultDatabase(connection, databaseOptions) };
}

View File

@ -96,6 +96,28 @@ import type { DatabaseNameSqlOptions, DropTableChildObjectSqlOptions, DropObject
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/databaseExport";
import type { DataCompareFromTablesOptions, DataCompareFromTablesPreparation, DataCompareSyncPlan, DataCompareSyncPlanOptions, DataComparePreparation, DataComparePreparationOptions } from "@/lib/dataCompare";
import type { DataGridSavePreparation } from "./tauri";
import type {
NacosConfigHistoryKey,
NacosConfigHistoryList,
NacosConfigHistoryQuery,
NacosConfigItem,
NacosConfigKey,
NacosConfigList,
NacosConfigQuery,
NacosConfigRollbackRequest,
NacosConfigUpsert,
NacosConnectionInfo,
NacosInstanceInfo,
NacosInstanceQuery,
NacosInstanceUpdate,
NacosNamespaceCreate,
NacosNamespaceInfo,
NacosNamespaceUpdate,
NacosRawRequest,
NacosRawResponse,
NacosServiceList,
NacosServiceQuery,
} from "@/types/nacos";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/safeStorage";
// ---------------------------------------------------------------------------
@ -1500,6 +1522,70 @@ export async function etcdDelete(connectionId: string, key: string): Promise<KvD
return post("/api/etcd/delete", { connectionId, key });
}
// ---------------------------------------------------------------------------
// Nacos
// ---------------------------------------------------------------------------
export async function nacosTestConnection(connectionId: string): Promise<NacosConnectionInfo> {
return post("/api/nacos/test-connection", { connectionId });
}
export async function nacosListNamespaces(connectionId: string): Promise<NacosNamespaceInfo[]> {
return post("/api/nacos/namespaces/list", { connectionId });
}
export async function nacosCreateNamespace(connectionId: string, req: NacosNamespaceCreate): Promise<void> {
return post("/api/nacos/namespaces/create", { connectionId, req });
}
export async function nacosUpdateNamespace(connectionId: string, req: NacosNamespaceUpdate): Promise<void> {
return post("/api/nacos/namespaces/update", { connectionId, req });
}
export async function nacosListConfigs(connectionId: string, query: NacosConfigQuery): Promise<NacosConfigList> {
return post("/api/nacos/configs/list", { connectionId, query });
}
export async function nacosGetConfig(connectionId: string, key: NacosConfigKey): Promise<NacosConfigItem> {
return post("/api/nacos/configs/get", { connectionId, key });
}
export async function nacosPublishConfig(connectionId: string, req: NacosConfigUpsert): Promise<void> {
return post("/api/nacos/configs/publish", { connectionId, req });
}
export async function nacosDeleteConfig(connectionId: string, key: NacosConfigKey): Promise<void> {
return post("/api/nacos/configs/delete", { connectionId, key });
}
export async function nacosListConfigHistory(connectionId: string, query: NacosConfigHistoryQuery): Promise<NacosConfigHistoryList> {
return post("/api/nacos/configs/history/list", { connectionId, query });
}
export async function nacosGetConfigHistory(connectionId: string, key: NacosConfigHistoryKey): Promise<NacosConfigItem> {
return post("/api/nacos/configs/history/get", { connectionId, key });
}
export async function nacosRollbackConfig(connectionId: string, req: NacosConfigRollbackRequest): Promise<void> {
return post("/api/nacos/configs/history/rollback", { connectionId, req });
}
export async function nacosListServices(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceList> {
return post("/api/nacos/services/list", { 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> {
return post("/api/nacos/instances/update", { connectionId, req });
}
export async function nacosRawRequest(connectionId: string, req: NacosRawRequest): Promise<NacosRawResponse> {
return post("/api/nacos/raw", { connectionId, req });
}
// ---------------------------------------------------------------------------
// MongoDB
// ---------------------------------------------------------------------------

View File

@ -0,0 +1,83 @@
import { invoke } from "@tauri-apps/api/core";
import type {
NacosConfigHistoryKey,
NacosConfigHistoryList,
NacosConfigHistoryQuery,
NacosConfigItem,
NacosConfigKey,
NacosConfigList,
NacosConfigQuery,
NacosConfigRollbackRequest,
NacosConfigUpsert,
NacosConnectionInfo,
NacosInstanceInfo,
NacosInstanceQuery,
NacosInstanceUpdate,
NacosNamespaceCreate,
NacosNamespaceInfo,
NacosNamespaceUpdate,
NacosRawRequest,
NacosRawResponse,
NacosServiceList,
NacosServiceQuery,
} from "@/types/nacos";
export async function nacosTestConnection(connectionId: string): Promise<NacosConnectionInfo> {
return invoke("nacos_test_connection", { connectionId });
}
export async function nacosListNamespaces(connectionId: string): Promise<NacosNamespaceInfo[]> {
return invoke("nacos_list_namespaces", { connectionId });
}
export async function nacosCreateNamespace(connectionId: string, req: NacosNamespaceCreate): Promise<void> {
return invoke("nacos_create_namespace", { connectionId, req });
}
export async function nacosUpdateNamespace(connectionId: string, req: NacosNamespaceUpdate): Promise<void> {
return invoke("nacos_update_namespace", { connectionId, req });
}
export async function nacosListConfigs(connectionId: string, query: NacosConfigQuery): Promise<NacosConfigList> {
return invoke("nacos_list_configs", { connectionId, query });
}
export async function nacosGetConfig(connectionId: string, key: NacosConfigKey): Promise<NacosConfigItem> {
return invoke("nacos_get_config", { connectionId, key });
}
export async function nacosPublishConfig(connectionId: string, req: NacosConfigUpsert): Promise<void> {
return invoke("nacos_publish_config", { connectionId, req });
}
export async function nacosDeleteConfig(connectionId: string, key: NacosConfigKey): Promise<void> {
return invoke("nacos_delete_config", { connectionId, key });
}
export async function nacosListConfigHistory(connectionId: string, query: NacosConfigHistoryQuery): Promise<NacosConfigHistoryList> {
return invoke("nacos_list_config_history", { connectionId, query });
}
export async function nacosGetConfigHistory(connectionId: string, key: NacosConfigHistoryKey): Promise<NacosConfigItem> {
return invoke("nacos_get_config_history", { connectionId, key });
}
export async function nacosRollbackConfig(connectionId: string, req: NacosConfigRollbackRequest): Promise<void> {
return invoke("nacos_rollback_config", { connectionId, req });
}
export async function nacosListServices(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceList> {
return invoke("nacos_list_services", { 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> {
return invoke("nacos_update_instance", { connectionId, req });
}
export async function nacosRawRequest(connectionId: string, req: NacosRawRequest): Promise<NacosRawResponse> {
return invoke("nacos_raw_request", { connectionId, req });
}

View File

@ -0,0 +1,323 @@
import type { NacosConfigHistoryItem, NacosConfigItem, NacosInstanceInfo, NacosRawRequest, NacosServiceInfo } from "@/types/nacos";
import { diffChars, diffLines } from "diff";
export type NacosRawTemplateKey = "serverState" | "namespaceList" | "configDetail" | "serviceList" | "instanceList";
export interface NacosRawTemplate {
key: NacosRawTemplateKey;
method: string;
path: string;
query: string;
body: string;
}
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: "configDetail",
method: "GET",
path: "/v3/console/cs/config",
query: "dataId=application.yaml&groupName=DEFAULT_GROUP&namespaceId=",
body: "",
},
{
key: "serviceList",
method: "GET",
path: "/v3/console/ns/service/list",
query: "pageNo=1&pageSize=20&namespaceId=",
body: "",
},
{
key: "instanceList",
method: "GET",
path: "/v3/console/ns/instance/list",
query: "serviceName=DEFAULT_GROUP@@example&namespaceId=",
body: "",
},
];
export function parseNacosRawQuery(text: string): Record<string, string> | undefined {
const trimmed = text.trim().replace(/^\?/, "");
if (!trimmed) return undefined;
return Object.fromEntries(new URLSearchParams(trimmed).entries());
}
export function parseNacosRawBody(text: string): unknown {
const trimmed = text.trim();
if (!trimmed) return undefined;
try {
return JSON.parse(trimmed);
} catch {
return text;
}
}
export function buildNacosRawRequest(method: string, path: string, queryText: string, bodyText: string): NacosRawRequest {
return {
method,
path: path.trim(),
query: parseNacosRawQuery(queryText),
body: parseNacosRawBody(bodyText),
};
}
export function isNacosRawMutation(method: string): boolean {
return method.trim().toUpperCase() !== "GET";
}
export function formatNacosConfigIdentity(item: Pick<NacosConfigItem, "namespace" | "dataId" | "group">, fallbackNamespace = ""): string {
return [`namespace=${item.namespace || fallbackNamespace || "public"}`, `dataId=${item.dataId}`, `group=${item.group || "DEFAULT_GROUP"}`].join("\n");
}
export function buildNacosConfigExport(item: NacosConfigItem, content: string): string {
return [`# namespace: ${item.namespace || "public"}`, `# dataId: ${item.dataId}`, `# group: ${item.group || "DEFAULT_GROUP"}`, item.configType ? `# type: ${item.configType}` : "", "", content].filter((line, index) => index >= 4 || line).join("\n");
}
export function buildNacosConfigCopy(item: NacosConfigItem, content: string): string {
return `${formatNacosConfigIdentity(item)}\n\n${content}`;
}
export function createNacosSaveAsCopy(item: NacosConfigItem): NacosConfigItem {
return {
...item,
dataId: item.dataId ? `${item.dataId}.copy` : "",
content: item.content ?? "",
};
}
export interface NacosDiffSummary {
changed: boolean;
addedLines: number;
removedLines: number;
preview: string;
}
export type NacosDiffLineType = "equal" | "delete" | "insert" | "modify" | "padding";
export interface NacosInlineSegment {
value: string;
changed: boolean;
}
export interface NacosSideBySideDiffRow {
id: string;
leftLineNumber: number | null;
rightLineNumber: number | null;
leftContent: string;
rightContent: string;
leftType: NacosDiffLineType;
rightType: NacosDiffLineType;
leftInline: NacosInlineSegment[];
rightInline: NacosInlineSegment[];
}
export interface NacosInlineDiffRow {
id: string;
lineNumber: number | null;
content: string;
type: Exclude<NacosDiffLineType, "modify" | "padding">;
segments: NacosInlineSegment[];
}
export function summarizeNacosConfigDiff(before: string, after: string, maxPreviewLines = 40): NacosDiffSummary {
if (before === after) {
return { changed: false, addedLines: 0, removedLines: 0, preview: "No content changes." };
}
const beforeLines = before.split(/\r?\n/);
const afterLines = after.split(/\r?\n/);
const max = Math.max(beforeLines.length, afterLines.length);
const lines: string[] = [];
let addedLines = 0;
let removedLines = 0;
for (let index = 0; index < max; index += 1) {
const left = beforeLines[index];
const right = afterLines[index];
if (left === right) continue;
if (left !== undefined) {
removedLines += 1;
lines.push(`- ${left}`);
}
if (right !== undefined) {
addedLines += 1;
lines.push(`+ ${right}`);
}
if (lines.length >= maxPreviewLines) {
lines.push("...");
break;
}
}
return { changed: true, addedLines, removedLines, preview: lines.join("\n") };
}
function normalizeNacosDiffText(value: string): string {
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
function splitDiffLines(value: string): string[] {
const lines = normalizeNacosDiffText(value).split("\n");
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
return lines;
}
function inlineSegments(left: string, right: string): { left: NacosInlineSegment[]; right: NacosInlineSegment[] } {
const changes = diffChars(left, right);
const leftSegments: NacosInlineSegment[] = [];
const rightSegments: NacosInlineSegment[] = [];
for (const change of changes) {
if (change.removed) {
leftSegments.push({ value: change.value, changed: true });
} else if (change.added) {
rightSegments.push({ value: change.value, changed: true });
} else {
leftSegments.push({ value: change.value, changed: false });
rightSegments.push({ value: change.value, changed: false });
}
}
return { left: leftSegments, right: rightSegments };
}
function pairChangedLines(leftLines: string[], rightLines: string[], leftStart: number, rightStart: number, rows: NacosSideBySideDiffRow[], nextId: () => string) {
const max = Math.max(leftLines.length, rightLines.length);
for (let index = 0; index < max; index += 1) {
const left = leftLines[index];
const right = rightLines[index];
const hasLeft = left !== undefined;
const hasRight = right !== undefined;
const inline = hasLeft && hasRight ? inlineSegments(left, right) : { left: [], right: [] };
rows.push({
id: nextId(),
leftLineNumber: hasLeft ? leftStart + index : null,
rightLineNumber: hasRight ? rightStart + index : null,
leftContent: left ?? "",
rightContent: right ?? "",
leftType: hasLeft ? (hasRight ? "modify" : "delete") : "padding",
rightType: hasRight ? (hasLeft ? "modify" : "insert") : "padding",
leftInline: inline.left,
rightInline: inline.right,
});
}
}
export function buildNacosSideBySideDiff(before: string, after: string): NacosSideBySideDiffRow[] {
const changes = diffLines(normalizeNacosDiffText(before), normalizeNacosDiffText(after), { newlineIsToken: false });
const rows: NacosSideBySideDiffRow[] = [];
let leftLineNumber = 1;
let rightLineNumber = 1;
let id = 0;
const nextId = () => `nacos-diff-${id++}`;
for (let index = 0; index < changes.length; index += 1) {
const change = changes[index];
if (!change.added && !change.removed) {
for (const line of splitDiffLines(change.value)) {
rows.push({
id: nextId(),
leftLineNumber,
rightLineNumber,
leftContent: line,
rightContent: line,
leftType: "equal",
rightType: "equal",
leftInline: [{ value: line, changed: false }],
rightInline: [{ value: line, changed: false }],
});
leftLineNumber += 1;
rightLineNumber += 1;
}
continue;
}
if (change.removed) {
const leftLines = splitDiffLines(change.value);
const next = changes[index + 1];
if (next?.added) {
const rightLines = splitDiffLines(next.value);
pairChangedLines(leftLines, rightLines, leftLineNumber, rightLineNumber, rows, nextId);
leftLineNumber += leftLines.length;
rightLineNumber += rightLines.length;
index += 1;
} else {
pairChangedLines(leftLines, [], leftLineNumber, rightLineNumber, rows, nextId);
leftLineNumber += leftLines.length;
}
continue;
}
if (change.added) {
const rightLines = splitDiffLines(change.value);
pairChangedLines([], rightLines, leftLineNumber, rightLineNumber, rows, nextId);
rightLineNumber += rightLines.length;
}
}
return rows;
}
function visibleInlineSegments(content: string, segments: NacosInlineSegment[]): NacosInlineSegment[] {
return segments.length ? segments : [{ value: content, changed: false }];
}
export function buildNacosInlineDiff(before: string, after: string): NacosInlineDiffRow[] {
return buildNacosSideBySideDiff(before, after).flatMap((row) => {
if (row.leftType === "equal" && row.rightType === "equal") {
return [
{
id: `${row.id}-equal`,
lineNumber: row.leftLineNumber,
content: row.leftContent,
type: "equal" as const,
segments: visibleInlineSegments(row.leftContent, row.leftInline),
},
];
}
const rows: NacosInlineDiffRow[] = [];
if (row.leftType === "delete" || row.leftType === "modify") {
rows.push({
id: `${row.id}-delete`,
lineNumber: row.leftLineNumber,
content: row.leftContent,
type: "delete",
segments: visibleInlineSegments(row.leftContent, row.leftInline),
});
}
if (row.rightType === "insert" || row.rightType === "modify") {
rows.push({
id: `${row.id}-insert`,
lineNumber: row.rightLineNumber,
content: row.rightContent,
type: "insert",
segments: visibleInlineSegments(row.rightContent, row.rightInline),
});
}
return rows;
});
}
export function buildNacosConfigDeleteConfirm(item: NacosConfigItem, fallbackNamespace = ""): string {
return formatNacosConfigIdentity(item, fallbackNamespace);
}
export function buildNacosConfigHistoryRollbackConfirm(item: NacosConfigHistoryItem, fallbackNamespace = ""): string {
return [`namespace=${item.namespace || fallbackNamespace || "public"}`, `dataId=${item.dataId}`, `group=${item.group || "DEFAULT_GROUP"}`, item.lastModifiedTime ? `historyTime=${item.lastModifiedTime}` : "", item.operator ? `operator=${item.operator}` : ""].filter(Boolean).join("\n");
}
export function buildNacosInstanceConfirm(service: NacosServiceInfo, instance: NacosInstanceInfo, patch: Partial<NacosInstanceInfo>, fallbackGroup = "", namespace = ""): string {
const targetEnabled = patch.enabled ?? instance.enabled;
const targetHealthy = patch.healthy ?? instance.healthy;
return [
`namespace=${namespace || "public"}`,
`serviceName=${service.serviceName}`,
`group=${instance.groupName || service.groupName || fallbackGroup || "DEFAULT_GROUP"}`,
`instance=${instance.ip}:${instance.port}`,
patch.enabled == null ? "" : `targetEnabled=${targetEnabled === false ? "false" : "true"}`,
patch.healthy == null ? "" : `targetHealthy=${targetHealthy === false ? "false" : "true"}`,
]
.filter(Boolean)
.join("\n");
}
export function buildNacosRawMutationConfirm(req: NacosRawRequest): string {
return [`method=${req.method.toUpperCase()}`, `path=${req.path}`, req.query ? `query=${JSON.stringify(req.query)}` : ""].filter(Boolean).join("\n");
}

View File

@ -33,6 +33,8 @@ export interface SavedOpenTab {
pinned?: boolean;
mode?: QueryTab["mode"];
mqTenant?: string;
nacosNamespace?: string;
nacosNamespaceName?: string;
structureTableName?: string;
objectBrowser?: QueryTab["objectBrowser"];
objectSource?: QueryTab["objectSource"];
@ -72,6 +74,8 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
pinned: tab.pinned,
mode: tab.mode,
...(tab.mqTenant !== undefined ? { mqTenant: tab.mqTenant } : {}),
...(tab.nacosNamespace !== undefined ? { nacosNamespace: tab.nacosNamespace } : {}),
...(tab.nacosNamespaceName !== undefined ? { nacosNamespaceName: tab.nacosNamespaceName } : {}),
...(tab.structureTableName !== undefined ? { structureTableName: tab.structureTableName } : {}),
objectBrowser: tab.objectBrowser,
objectSource: tab.objectSource,

View File

@ -29,6 +29,11 @@ export type ActiveTabSidebarTarget =
connectionId: string;
tenant: string;
}
| {
type: "nacos-namespace";
connectionId: string;
namespace: string;
}
| {
type: "query-context";
connectionId: string;
@ -84,6 +89,10 @@ export function activeTabSidebarTarget(tab: QueryTab | undefined | null): Active
return { type: "mq-tenant", connectionId: tab.connectionId, tenant: tab.mqTenant };
}
if (tab.mode === "nacos") {
return { type: "nacos-namespace", connectionId: tab.connectionId, namespace: tab.nacosNamespace || "" };
}
if (tab.savedSqlId) {
return { type: "saved-sql-file", savedSqlId: tab.savedSqlId };
}
@ -133,6 +142,10 @@ export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): b
return node.type === "mq-tenant" && node.connectionId === target.connectionId && (node.mqTenant || node.label) === target.tenant;
}
if (target.type === "nacos-namespace") {
return node.type === "nacos-namespace" && node.connectionId === target.connectionId && (node.nacosNamespace || "") === target.namespace;
}
if (target.type === "saved-sql-file") {
return node.type === "saved-sql-file" && node.savedSqlId === target.savedSqlId;
}

View File

@ -181,6 +181,7 @@ export function tabModeLabel(tab: QueryTab, t: Translate): string {
if (tab.mode === "vector") return t("tabs.vector");
if (tab.mode === "redis") return t("tabs.redis");
if (tab.mode === "etcd") return t("tabs.etcd");
if (tab.mode === "nacos") return "Nacos";
if (tab.mode === "objects") return t("tabs.objects");
if (tab.mode === "users") return t("tabs.users");
return tab.mode;

View File

@ -1843,3 +1843,4 @@ export async function exportQueryResultMarkdown(filePath: string, columns: strin
}
export * from "./mq-tauri";
export * from "./nacos-tauri";

View File

@ -879,6 +879,8 @@ export const useConnectionStore = defineStore("connection", () => {
await loadVectorCollections(connectionId);
} else if (config.db_type === "mq") {
await loadMqTenants(connectionId, { force: true });
} else if (config.db_type === "nacos") {
await loadNacosNamespaces(connectionId, { force: true });
} else {
await loadDatabases(connectionId, { force: true });
}
@ -1228,6 +1230,47 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadNacosNamespaces(connectionId: string, options?: LoadTreeOptions) {
const node = findNode(treeNodes.value, connectionId);
if (!node) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
if (useCachedChildren(node, options)) return;
const namespaces = await api.nacosListNamespaces(connectionId);
const sorted = [...namespaces].sort((left, right) => {
const leftLabel = left.namespaceShowName || left.namespace || "public";
const rightLabel = right.namespaceShowName || right.namespace || "public";
return leftLabel.localeCompare(rightLabel);
});
setChildren(
node,
sorted.map((namespace) => {
const value = namespace.namespace || "";
const label = namespace.namespaceShowName || value || "public";
return {
id: schemaCacheKey(connectionId, "nacos-namespace", value || "public"),
label,
type: "nacos-namespace" as const,
connectionId,
nacosNamespace: value,
nacosNamespaceName: label,
comment: namespace.namespaceDesc || null,
objectCount: namespace.configCount,
};
}),
);
node.isExpanded = true;
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
} finally {
node.isLoading = false;
}
}
function updateRedisDbKeyStats(connectionId: string, db: number, stats: { loaded?: number; total?: number; totalDelta?: number }) {
const node = findNode(treeNodes.value, `${connectionId}:db${db}`);
if (!node || node.type !== "redis-db") return;
@ -1996,6 +2039,8 @@ export const useConnectionStore = defineStore("connection", () => {
await loadVectorCollections(node.connectionId);
} else if (config?.db_type === "mq") {
await loadMqTenants(node.connectionId, options);
} else if (config?.db_type === "nacos") {
await loadNacosNamespaces(node.connectionId, options);
} else {
await loadDatabases(node.connectionId, options);
}
@ -3266,6 +3311,7 @@ export const useConnectionStore = defineStore("connection", () => {
refreshRedisDbKeyCounts,
loadEtcdRoot,
loadMqTenants,
loadNacosNamespaces,
updateRedisDbKeyStats,
loadMongoDatabases,
loadElasticsearchIndices,

View File

@ -603,6 +603,37 @@ export const useQueryStore = defineStore("query", () => {
return id;
}
function openNacosAdmin(connectionId: string, target?: { namespace?: string; namespaceName?: string }) {
const namespace = target?.namespace ?? "";
const namespaceName = target?.namespaceName || (namespace ? namespace : "public");
const existing = tabs.value.find((tab) => tab.mode === "nacos" && tab.connectionId === connectionId && (tab.nacosNamespace || "") === namespace);
if (existing) {
existing.nacosNamespaceName = namespaceName;
if (!existing.customTitle) existing.title = `${useConnectionStore().getConfig(connectionId)?.name || "Nacos"}:${namespaceName}`;
activeTabId.value = existing.id;
return existing.id;
}
const conn = useConnectionStore().getConfig(connectionId);
const id = uuid();
const tab: QueryTab = {
id,
title: `${conn?.name || "Nacos"}:${namespaceName}`,
connectionId,
database: conn?.database || "",
sql: "",
isExecuting: false,
isCancelling: false,
isExplaining: false,
mode: "nacos",
nacosNamespace: namespace,
nacosNamespaceName: namespaceName,
};
tabs.value.push(tab);
activeTabId.value = id;
return id;
}
function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string) {
const resolvedTableName = tableName || "";
if (resolvedTableName) {
@ -769,6 +800,8 @@ export const useQueryStore = defineStore("query", () => {
explainExecutionId: undefined,
mode: original.mode,
mqTenant: original.mqTenant,
nacosNamespace: original.nacosNamespace,
nacosNamespaceName: original.nacosNamespaceName,
structureTableName: original.structureTableName,
objectBrowser: original.objectBrowser ? { ...original.objectBrowser } : undefined,
objectSource: original.objectSource ? { ...original.objectSource } : undefined,
@ -2189,6 +2222,7 @@ export const useQueryStore = defineStore("query", () => {
openObjectBrowser,
openUserAdmin,
openMqAdmin,
openNacosAdmin,
openTableStructure,
linkSavedSql,
openSavedSql,

View File

@ -56,7 +56,8 @@ export type DatabaseType =
| "iris"
| "influxdb"
| "jdbc"
| "mq";
| "mq"
| "nacos";
export interface SqlSnippet {
id: string;
@ -484,6 +485,7 @@ export type TreeNodeType =
| "trigger"
| "redis-db"
| "mq-tenant"
| "nacos-namespace"
| "etcd-root"
| "mongo-db"
| "mongo-collection"
@ -517,6 +519,8 @@ export interface TreeNode {
linkedCatalog?: string;
linkedSchema?: string;
mqTenant?: string;
nacosNamespace?: string;
nacosNamespaceName?: string;
schema?: string;
tableName?: string;
tableType?: string;
@ -591,8 +595,10 @@ export interface QueryTab {
executionId?: string;
isExplaining?: boolean;
explainExecutionId?: string;
mode: "data" | "query" | "redis" | "mongo" | "vector" | "etcd" | "mq" | "objects" | "structure" | "users";
mode: "data" | "query" | "redis" | "mongo" | "vector" | "etcd" | "mq" | "nacos" | "objects" | "structure" | "users";
mqTenant?: string;
nacosNamespace?: string;
nacosNamespaceName?: string;
structureTableName?: string;
objectBrowser?: {
schema?: string;

View File

@ -0,0 +1,200 @@
export interface NacosCapabilities {
supportsConfigManagement: boolean;
supportsConfigHistory?: boolean;
supportsServiceManagement: boolean;
supportsInstanceUpdate: boolean;
supportsRawApi: boolean;
}
export interface NacosConnectionInfo {
serverAddr: string;
namespace: string;
serverVersion?: string;
auth: string;
capabilities: NacosCapabilities;
raw?: unknown;
}
export interface NacosNamespaceInfo {
namespace: string;
namespaceShowName: string;
namespaceDesc?: string;
configCount?: number;
quota?: number;
namespaceType?: number;
}
export interface NacosNamespaceCreate {
namespaceId?: string;
namespaceName: string;
namespaceDesc?: string;
}
export interface NacosNamespaceUpdate {
namespaceId: string;
namespaceName: string;
namespaceDesc?: string;
}
export interface NacosAuthConfig {
kind: "none" | "usernamePassword";
username?: string;
password?: string;
}
export interface NacosAdminConfig {
serverAddr: string;
namespace?: string;
contextPath?: string;
auth?: NacosAuthConfig;
tlsSkipVerify?: boolean;
pageSize?: number;
}
export interface NacosConfigQuery {
namespace?: string;
group?: string;
dataId?: string;
search?: string;
pageNo?: number;
pageSize?: number;
}
export interface NacosConfigItem {
dataId: string;
group: string;
namespace: string;
appName?: string;
desc?: string;
tags?: string;
configType?: string;
md5?: string;
encryptedDataKey?: string;
content?: string;
}
export interface NacosConfigList {
pageNo: number;
pageSize: number;
totalCount: number;
items: NacosConfigItem[];
}
export interface NacosConfigKey {
namespace?: string;
dataId: string;
group: string;
}
export interface NacosConfigUpsert extends NacosConfigKey {
content: string;
configType?: string;
appName?: string;
desc?: string;
tags?: string;
}
export interface NacosConfigHistoryQuery extends NacosConfigKey {
pageNo?: number;
pageSize?: number;
}
export interface NacosConfigHistoryItem {
historyId: string;
nid?: number;
dataId: string;
group: string;
namespace: string;
appName?: string;
operation?: string;
operator?: string;
lastModifiedTime?: string;
configType?: string;
tags?: string;
md5?: string;
}
export interface NacosConfigHistoryList {
pageNo: number;
pageSize: number;
totalCount: number;
items: NacosConfigHistoryItem[];
}
export interface NacosConfigHistoryKey extends NacosConfigKey {
historyId: string;
nid?: number;
}
export interface NacosConfigRollbackRequest extends NacosConfigHistoryKey {}
export interface NacosServiceQuery {
namespace?: string;
groupName?: string;
serviceName?: string;
pageNo?: number;
pageSize?: number;
}
export interface NacosServiceInfo {
serviceName: string;
groupName?: string;
clusterCount?: number;
ipCount?: number;
healthyInstanceCount?: number;
triggerFlag?: string;
}
export interface NacosServiceList {
pageNo: number;
pageSize: number;
totalCount: number;
items: NacosServiceInfo[];
}
export interface NacosInstanceQuery {
namespace?: string;
serviceName: string;
groupName?: string;
clusters?: string;
}
export interface NacosInstanceInfo {
ip: string;
port: number;
serviceName?: string;
clusterName?: string;
groupName?: string;
healthy?: boolean;
enabled?: boolean;
ephemeral?: boolean;
weight?: number;
metadata?: unknown;
}
export interface NacosInstanceUpdate {
namespace?: string;
serviceName: string;
ip: string;
port: number;
groupName?: string;
clusterName?: string;
healthy?: boolean;
enabled?: boolean;
ephemeral?: boolean;
weight?: number;
metadata?: unknown;
}
export interface NacosRawRequest {
method: string;
path: string;
query?: Record<string, string>;
body?: unknown;
}
export interface NacosRawResponse {
status: number;
body: unknown;
text?: string;
}

View File

@ -80,6 +80,8 @@ pub enum PoolKind {
/// Message queue admin connection (not a data query pool; serves as a
/// marker that this connection_id is a valid MQ admin connection).
MessageQueue,
/// Nacos admin connection marker.
Nacos,
}
macro_rules! agent_connection_pool_database_type {
@ -131,6 +133,7 @@ pub struct AppState {
pub storage: Storage,
pub plugins: PluginRegistry,
pub agent_manager: crate::agent_manager::AgentManager,
pub nacos_registry: crate::nacos::NacosAdminRegistry,
#[cfg(feature = "mq-admin")]
pub mq_registry: crate::mq::MqAdminRegistry,
}
@ -358,6 +361,7 @@ impl AppState {
agent_dir,
app_version,
),
nacos_registry: crate::nacos::NacosAdminRegistry::new(),
#[cfg(feature = "mq-admin")]
mq_registry: crate::mq::MqAdminRegistry::new(),
}
@ -781,6 +785,12 @@ impl AppState {
db::influxdb_driver::test_connection(&client, connect_timeout).await?;
PoolKind::InfluxDb(client)
}
DatabaseType::Nacos => {
let admin_config = self.nacos_admin_config_for_connection(connection_id, &config).await?;
let adapter = self.nacos_registry.build_transient_config(admin_config).await?;
adapter.test_connection().await?;
PoolKind::Nacos
}
agent_connection_pool_database_type!() => {
let connect_params =
agent_connect_params(&db_config, &host, port, db_config.effective_database().unwrap_or(""));
@ -1014,6 +1024,20 @@ impl AppState {
Ok(mqc.with_connect_override(&host, port))
}
pub async fn nacos_admin_config_for_connection(
&self,
connection_id: &str,
config: &ConnectionConfig,
) -> Result<crate::nacos::config::NacosAdminConfig, String> {
let nacos_config = crate::nacos::config::NacosAdminConfig::from_connection(config)?;
if !config.has_effective_transport_layers() {
return Ok(nacos_config);
}
let (host, port) = self.connection_host_port(connection_id, config).await?;
Ok(nacos_config.with_connect_override(&host, port))
}
async fn remove_stale_connection_pool(&self, pool_key: &str) -> bool {
if self.running_queries.is_pool_active(pool_key) {
return false;
@ -1191,7 +1215,8 @@ impl AppState {
| PoolKind::DuckDb(_)
| PoolKind::ExternalTabular(_)
| PoolKind::ExternalDriver { .. }
| PoolKind::MessageQueue => false,
| PoolKind::MessageQueue
| PoolKind::Nacos => false,
}
};
@ -1570,7 +1595,8 @@ impl AppState {
| PoolKind::DuckDb(_)
| PoolKind::ExternalTabular(_)
| PoolKind::ExternalDriver { .. }
| PoolKind::MessageQueue => true,
| PoolKind::MessageQueue
| PoolKind::Nacos => true,
PoolKind::Redis(_) => unreachable!("Redis handled separately"),
};
if !healthy {
@ -1795,6 +1821,8 @@ fn connection_remote_endpoint(config: &ConnectionConfig) -> (String, u16) {
.unwrap_or_else(|| (config.host.clone(), config.port))
} else if config.db_type == DatabaseType::MessageQueue {
parse_mq_admin_host_port(config).unwrap_or_else(|| (config.host.clone(), config.port))
} else if config.db_type == DatabaseType::Nacos {
parse_nacos_server_host_port(config).unwrap_or_else(|| (config.host.clone(), config.port))
} else {
(config.host.clone(), config.port)
}
@ -1817,6 +1845,23 @@ fn parse_mq_admin_host_port(config: &ConnectionConfig) -> Option<(String, u16)>
Some((host, port))
}
fn parse_nacos_server_host_port(config: &ConnectionConfig) -> Option<(String, u16)> {
let value = config
.external_config
.as_ref()?
.get("serverAddr")
.or_else(|| config.external_config.as_ref()?.get("server_addr"))?
.as_str()?
.trim();
if value.is_empty() {
return None;
}
let url = reqwest::Url::parse(value).ok()?;
let host = url.host_str()?.to_string();
let port = url.port_or_known_default()?;
Some((host, port))
}
fn normalize_client_session_id(client_session_id: Option<&str>) -> Option<String> {
client_session_id.map(str::trim).filter(|session| !session.is_empty()).map(|session| session.replace(':', "_"))
}
@ -1891,6 +1936,7 @@ fn clone_pool_kind(pool: &PoolKind) -> PoolKind {
PoolKind::ExternalDriver { driver_id: driver_id.clone(), config: config.clone(), session: session.clone() }
}
PoolKind::MessageQueue => PoolKind::MessageQueue,
PoolKind::Nacos => PoolKind::Nacos,
PoolKind::Redis(_) => panic!("clone_pool_kind not supported for Redis — handled separately"),
}
}
@ -1940,6 +1986,7 @@ pub async fn close_pool_kind(pool: PoolKind) {
session.shutdown().await;
}
PoolKind::MessageQueue => {}
PoolKind::Nacos => {}
}
}
@ -3242,6 +3289,28 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}
#[tokio::test]
async fn nacos_admin_config_allows_domain_server_addr_without_transport_override() {
let (state, dir) = test_app_state().await;
let mut config = mysql_config(None);
config.id = "aliyun-nacos".to_string();
config.db_type = DatabaseType::Nacos;
config.host = "example.com".to_string();
config.port = 8848;
config.external_config = Some(serde_json::json!({
"serverAddr": "https://nacos.aliyuncs.com:8848",
"namespace": "public",
"contextPath": "/nacos",
"auth": { "kind": "none" }
}));
let nacos_config = state.nacos_admin_config_for_connection("aliyun-nacos", &config).await.unwrap();
assert_eq!(nacos_config.server_addr, "https://nacos.aliyuncs.com:8848");
assert!(nacos_config.connect_override.is_none());
let _ = std::fs::remove_dir_all(dir);
}
#[tokio::test]
#[ignore = "requires a reachable GaussDB instance via environment variables"]
async fn live_gaussdb_native_connection_succeeds() {

View File

@ -29,6 +29,7 @@ pub mod models;
pub mod mongo_ops;
#[cfg(feature = "mq-admin")]
pub mod mq;
pub mod nacos;
pub mod object_source_sql;
pub mod path_utils;
pub mod plugins;

View File

@ -307,6 +307,7 @@ pub enum DatabaseType {
Xugu,
Iotdb,
Etcd,
Nacos,
#[serde(rename = "iris")]
Iris,
#[serde(rename = "turso")]
@ -793,6 +794,7 @@ impl ConnectionConfig {
}
DatabaseType::Jdbc => "jdbc:<redacted>".to_string(),
DatabaseType::MessageQueue => self.message_queue_admin_url(),
DatabaseType::Nacos => self.nacos_admin_url(),
}
}
@ -998,6 +1000,7 @@ impl ConnectionConfig {
self.connection_string.as_deref().filter(|value| !value.is_empty()).unwrap_or("jdbc:").to_string()
}
DatabaseType::MessageQueue => self.message_queue_admin_url(),
DatabaseType::Nacos => self.nacos_admin_url(),
}
}
@ -1012,6 +1015,17 @@ impl ConnectionConfig {
.to_string()
}
fn nacos_admin_url(&self) -> String {
self.external_config
.as_ref()
.and_then(|value| value.get("serverAddr").or_else(|| value.get("server_addr")))
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("nacos://")
.to_string()
}
fn normalized_url_params(&self) -> String {
let value = self.url_params.as_deref().unwrap_or("").trim();
if self.needs_bare_mysql() {

View File

@ -0,0 +1,193 @@
use serde::{Deserialize, Serialize};
use crate::models::connection::ConnectionConfig;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum NacosAuthConfig {
None,
UsernamePassword { username: String, password: String },
}
impl Default for NacosAuthConfig {
fn default() -> Self {
Self::None
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosAdminConfig {
pub server_addr: String,
#[serde(default)]
pub namespace: String,
#[serde(default)]
pub context_path: String,
#[serde(default)]
pub auth: NacosAuthConfig,
#[serde(default)]
pub tls_skip_verify: bool,
#[serde(default = "default_page_size")]
pub page_size: u32,
#[serde(skip)]
pub connect_override: Option<NacosConnectOverride>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NacosConnectOverride {
pub host: String,
pub port: u16,
}
pub fn default_page_size() -> u32 {
20
}
impl NacosAdminConfig {
pub fn from_connection(cfg: &ConnectionConfig) -> Result<Self, String> {
let parsed = if let Some(raw) = cfg.external_config.as_ref() {
serde_json::from_value::<NacosAdminConfig>(raw.clone())
.map_err(|e| format!("Failed to parse Nacos admin config: {e}"))?
} else {
let scheme = if cfg.ssl { "https" } else { "http" };
NacosAdminConfig {
server_addr: format!("{scheme}://{}:{}", cfg.host.trim(), cfg.port),
namespace: cfg.database.clone().unwrap_or_default(),
context_path: String::new(),
auth: if cfg.username.trim().is_empty() {
NacosAuthConfig::None
} else {
NacosAuthConfig::UsernamePassword { username: cfg.username.clone(), password: cfg.password.clone() }
},
tls_skip_verify: false,
page_size: default_page_size(),
connect_override: None,
}
};
parsed.validate()
}
pub fn validate(mut self) -> Result<Self, String> {
self.server_addr = self.server_addr.trim().trim_end_matches('/').to_string();
if self.server_addr.is_empty() {
return Err("Nacos server address is empty".to_string());
}
self.context_path = normalize_context_path(&self.context_path);
if self.page_size == 0 {
self.page_size = default_page_size();
}
self.page_size = self.page_size.clamp(1, 500);
Ok(self)
}
pub fn with_connect_override(mut self, host: &str, port: u16) -> Self {
self.connect_override = Some(NacosConnectOverride { host: host.to_string(), port });
self
}
}
pub fn normalize_context_path(path: &str) -> String {
let trimmed = path.trim().trim_end_matches('/');
if trimmed.is_empty() {
return String::new();
}
if trimmed.starts_with('/') {
trimmed.to_string()
} else {
format!("/{trimmed}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::connection::{default_keepalive_interval_secs, DatabaseType};
fn connection_with_external(value: serde_json::Value) -> ConnectionConfig {
ConnectionConfig {
id: "nacos-1".to_string(),
name: "Nacos".to_string(),
db_type: DatabaseType::Nacos,
driver_profile: None,
driver_label: None,
url_params: None,
host: "127.0.0.1".to_string(),
port: 8848,
username: String::new(),
password: String::new(),
database: None,
visible_databases: None,
attached_databases: Vec::new(),
color: None,
transport_layers: Vec::new(),
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
keepalive_interval_secs: default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
client_cert_path: String::new(),
client_key_path: String::new(),
sysdba: false,
oracle_connection_type: None,
connection_string: None,
redis_connection_mode: None,
redis_sentinel_master: String::new(),
redis_sentinel_nodes: String::new(),
redis_sentinel_username: String::new(),
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
external_config: Some(value),
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
one_time: false,
read_only: false,
}
}
#[test]
fn parses_external_config() {
let cfg = connection_with_external(serde_json::json!({
"serverAddr": " http://127.0.0.1:8848/ ",
"namespace": "public",
"contextPath": "nacos",
"pageSize": 100,
"auth": { "kind": "usernamePassword", "username": "nacos", "password": "pw" }
}));
let parsed = NacosAdminConfig::from_connection(&cfg).unwrap();
assert_eq!(parsed.server_addr, "http://127.0.0.1:8848");
assert_eq!(parsed.context_path, "/nacos");
assert_eq!(parsed.page_size, 100);
assert_eq!(parsed.namespace, "public");
}
#[test]
fn missing_external_context_path_defaults_to_root() {
let cfg = connection_with_external(serde_json::json!({
"serverAddr": "http://127.0.0.1:8848",
"auth": { "kind": "none" }
}));
let parsed = NacosAdminConfig::from_connection(&cfg).unwrap();
assert_eq!(parsed.context_path, "");
}
#[test]
fn falls_back_to_connection_fields() {
let mut cfg = connection_with_external(serde_json::Value::Null);
cfg.external_config = None;
cfg.username = "nacos".to_string();
cfg.password = "pw".to_string();
let parsed = NacosAdminConfig::from_connection(&cfg).unwrap();
assert_eq!(parsed.server_addr, "http://127.0.0.1:8848");
assert_eq!(parsed.context_path, "");
assert!(matches!(parsed.auth, NacosAuthConfig::UsernamePassword { .. }));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
//! Nacos admin console support.
//!
//! The public API is intentionally owned by dbx (`NacosAdmin`) instead of
//! exposing SDK/OpenAPI types. The current adapter uses Nacos OpenAPI because
//! the current nacos-sdk-rust releases require Rust edition 2024, while dbx
//! still supports an older Rust toolchain. A future SDK adapter can implement
//! the same port without changing commands, routes, or frontend contracts.
pub mod config;
pub mod http;
pub mod port;
pub mod service;
pub mod types;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use crate::models::connection::ConnectionConfig;
use crate::nacos::config::NacosAdminConfig;
use crate::nacos::http::NacosOpenApiAdmin;
use crate::nacos::port::NacosAdmin;
pub use crate::nacos::config::{NacosAdminConfig as NacosConfig, NacosAuthConfig};
pub use crate::nacos::types::*;
#[derive(Default)]
pub struct NacosAdminRegistry {
instances: RwLock<HashMap<String, Arc<dyn NacosAdmin>>>,
build_locks: RwLock<HashMap<String, Arc<Mutex<()>>>>,
}
impl NacosAdminRegistry {
pub fn new() -> Self {
Self { instances: RwLock::new(HashMap::new()), build_locks: RwLock::new(HashMap::new()) }
}
pub async fn get_or_build(&self, cfg: &ConnectionConfig) -> Result<Arc<dyn NacosAdmin>, String> {
let admin_config = NacosAdminConfig::from_connection(cfg)?;
self.get_or_build_config(&cfg.id, admin_config).await
}
pub async fn get_or_build_config(
&self,
connection_id: &str,
cfg: NacosAdminConfig,
) -> Result<Arc<dyn NacosAdmin>, String> {
if let Some(admin) = self.instances.read().await.get(connection_id) {
return Ok(admin.clone());
}
let lock = {
let mut locks = self.build_locks.write().await;
locks.entry(connection_id.to_string()).or_insert_with(|| Arc::new(Mutex::new(()))).clone()
};
let _guard = lock.lock().await;
if let Some(admin) = self.instances.read().await.get(connection_id) {
return Ok(admin.clone());
}
let admin = build_admin(cfg)?;
self.instances.write().await.insert(connection_id.to_string(), admin.clone());
Ok(admin)
}
pub async fn build_transient(&self, cfg: &ConnectionConfig) -> Result<Arc<dyn NacosAdmin>, String> {
let admin_config = NacosAdminConfig::from_connection(cfg)?;
self.build_transient_config(admin_config).await
}
pub async fn build_transient_config(&self, cfg: NacosAdminConfig) -> Result<Arc<dyn NacosAdmin>, String> {
build_admin(cfg)
}
pub async fn drop_connection(&self, connection_id: &str) {
self.instances.write().await.remove(connection_id);
self.build_locks.write().await.remove(connection_id);
}
}
fn build_admin(cfg: NacosAdminConfig) -> Result<Arc<dyn NacosAdmin>, String> {
Ok(Arc::new(NacosOpenApiAdmin::new(cfg)?))
}

View File

@ -0,0 +1,22 @@
use async_trait::async_trait;
use crate::nacos::types::*;
#[async_trait]
pub trait NacosAdmin: Send + Sync {
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>;
async fn update_namespace(&self, req: NacosNamespaceUpdate) -> Result<(), String>;
async fn list_configs(&self, query: NacosConfigQuery) -> Result<NacosConfigList, String>;
async fn get_config(&self, key: NacosConfigKey) -> Result<NacosConfigItem, String>;
async fn publish_config(&self, req: NacosConfigUpsert) -> Result<(), String>;
async fn delete_config(&self, key: NacosConfigKey) -> Result<(), String>;
async fn list_config_history(&self, query: NacosConfigHistoryQuery) -> Result<NacosConfigHistoryList, String>;
async fn get_config_history(&self, key: NacosConfigHistoryKey) -> Result<NacosConfigItem, String>;
async fn rollback_config(&self, req: NacosConfigRollbackRequest) -> Result<(), String>;
async fn list_services(&self, query: NacosServiceQuery) -> Result<NacosServiceList, String>;
async fn list_instances(&self, query: NacosInstanceQuery) -> Result<Vec<NacosInstanceInfo>, String>;
async fn update_instance(&self, req: NacosInstanceUpdate) -> Result<(), String>;
async fn raw_request(&self, req: NacosRawRequest) -> Result<NacosRawResponse, String>;
}

View File

@ -0,0 +1,287 @@
use crate::connection::AppState;
use crate::models::connection::DatabaseType;
use crate::nacos::types::*;
pub async fn nacos_test_connection_core(state: &AppState, conn_id: &str) -> Result<NacosConnectionInfo, String> {
let cfg = state.configs.read().await.get(conn_id).cloned().ok_or("Connection not found")?;
let admin = state.nacos_registry.build_transient(&cfg).await?;
admin.test_connection().await
}
pub async fn nacos_list_namespaces_core(state: &AppState, conn_id: &str) -> Result<Vec<NacosNamespaceInfo>, String> {
let admin = get_admin(state, conn_id).await?;
admin.list_namespaces().await
}
pub async fn nacos_create_namespace_core(
state: &AppState,
conn_id: &str,
req: NacosNamespaceCreate,
) -> Result<(), String> {
ensure_connection_writable(state, conn_id, "Create Nacos namespace").await?;
let admin = get_admin(state, conn_id).await?;
admin.create_namespace(req).await
}
pub async fn nacos_update_namespace_core(
state: &AppState,
conn_id: &str,
req: NacosNamespaceUpdate,
) -> Result<(), String> {
ensure_connection_writable(state, conn_id, "Update Nacos namespace").await?;
let admin = get_admin(state, conn_id).await?;
admin.update_namespace(req).await
}
pub async fn nacos_list_configs_core(
state: &AppState,
conn_id: &str,
query: NacosConfigQuery,
) -> Result<NacosConfigList, String> {
let admin = get_admin(state, conn_id).await?;
admin.list_configs(query).await
}
pub async fn nacos_get_config_core(
state: &AppState,
conn_id: &str,
key: NacosConfigKey,
) -> Result<NacosConfigItem, String> {
let admin = get_admin(state, conn_id).await?;
admin.get_config(key).await
}
pub async fn nacos_publish_config_core(state: &AppState, conn_id: &str, req: NacosConfigUpsert) -> Result<(), String> {
ensure_connection_writable(state, conn_id, "Publish Nacos config").await?;
let admin = get_admin(state, conn_id).await?;
admin.publish_config(req).await
}
pub async fn nacos_delete_config_core(state: &AppState, conn_id: &str, key: NacosConfigKey) -> Result<(), String> {
ensure_connection_writable(state, conn_id, "Delete Nacos config").await?;
let admin = get_admin(state, conn_id).await?;
admin.delete_config(key).await
}
pub async fn nacos_list_config_history_core(
state: &AppState,
conn_id: &str,
query: NacosConfigHistoryQuery,
) -> Result<NacosConfigHistoryList, String> {
let admin = get_admin(state, conn_id).await?;
admin.list_config_history(query).await
}
pub async fn nacos_get_config_history_core(
state: &AppState,
conn_id: &str,
key: NacosConfigHistoryKey,
) -> Result<NacosConfigItem, String> {
let admin = get_admin(state, conn_id).await?;
admin.get_config_history(key).await
}
pub async fn nacos_rollback_config_core(
state: &AppState,
conn_id: &str,
req: NacosConfigRollbackRequest,
) -> Result<(), String> {
ensure_connection_writable(state, conn_id, "Rollback Nacos config").await?;
let admin = get_admin(state, conn_id).await?;
admin.rollback_config(req).await
}
pub async fn nacos_list_services_core(
state: &AppState,
conn_id: &str,
query: NacosServiceQuery,
) -> Result<NacosServiceList, String> {
let admin = get_admin(state, conn_id).await?;
admin.list_services(query).await
}
pub async fn nacos_list_instances_core(
state: &AppState,
conn_id: &str,
query: NacosInstanceQuery,
) -> Result<Vec<NacosInstanceInfo>, String> {
let admin = get_admin(state, conn_id).await?;
admin.list_instances(query).await
}
pub async fn nacos_update_instance_core(
state: &AppState,
conn_id: &str,
req: NacosInstanceUpdate,
) -> Result<(), String> {
ensure_connection_writable(state, conn_id, "Update Nacos instance").await?;
let admin = get_admin(state, conn_id).await?;
admin.update_instance(req).await
}
pub async fn nacos_raw_request_core(
state: &AppState,
conn_id: &str,
req: NacosRawRequest,
) -> Result<NacosRawResponse, String> {
crate::nacos::http::validate_raw_api_path(&req.path)?;
if req.method.to_ascii_uppercase() != "GET" {
ensure_connection_writable(state, conn_id, "Run mutating Nacos raw request").await?;
}
let admin = get_admin(state, conn_id).await?;
admin.raw_request(req).await
}
async fn get_admin(
state: &AppState,
conn_id: &str,
) -> Result<std::sync::Arc<dyn crate::nacos::port::NacosAdmin>, String> {
let cfg = state.configs.read().await.get(conn_id).cloned().ok_or("Connection not found")?;
if cfg.db_type != DatabaseType::Nacos {
return Err("Connection is not a Nacos admin connection".to_string());
}
state.nacos_registry.get_or_build(&cfg).await
}
async fn ensure_connection_writable(state: &AppState, conn_id: &str, action: &str) -> Result<(), String> {
let cfg = state.configs.read().await.get(conn_id).cloned().ok_or("Connection not found")?;
if cfg.read_only {
Err(format!("{action} is blocked because this connection is read-only"))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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()));
std::fs::create_dir_all(&dir).unwrap();
let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap();
let state = AppState::new(storage);
let mut cfg = crate::models::connection::ConnectionConfig {
id: "nacos-1".to_string(),
name: "Nacos".to_string(),
db_type: DatabaseType::Nacos,
driver_profile: None,
driver_label: None,
url_params: None,
host: "127.0.0.1".to_string(),
port: 8848,
username: String::new(),
password: String::new(),
database: None,
visible_databases: None,
attached_databases: Vec::new(),
color: None,
transport_layers: Vec::new(),
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
client_cert_path: String::new(),
client_key_path: String::new(),
sysdba: false,
oracle_connection_type: None,
connection_string: None,
redis_connection_mode: None,
redis_sentinel_master: String::new(),
redis_sentinel_nodes: String::new(),
redis_sentinel_username: String::new(),
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
external_config: Some(serde_json::json!({ "serverAddr": "http://127.0.0.1:9" })),
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
one_time: false,
read_only: true,
};
cfg.read_only = true;
state.configs.write().await.insert(cfg.id.clone(), cfg);
let err = nacos_raw_request_core(
&state,
"nacos-1",
NacosRawRequest { method: "POST".to_string(), path: "/v1/cs/configs".to_string(), query: None, body: None },
)
.await
.unwrap_err();
assert!(err.contains("read-only"));
}
#[tokio::test]
async fn config_rollback_requires_writable_connection_before_adapter_build() {
let dir = std::env::temp_dir().join(format!("dbx-nacos-service-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap();
let state = AppState::new(storage);
let cfg = crate::models::connection::ConnectionConfig {
id: "nacos-rollback".to_string(),
name: "Nacos".to_string(),
db_type: DatabaseType::Nacos,
driver_profile: None,
driver_label: None,
url_params: None,
host: "127.0.0.1".to_string(),
port: 8848,
username: String::new(),
password: String::new(),
database: None,
visible_databases: None,
attached_databases: Vec::new(),
color: None,
transport_layers: Vec::new(),
connect_timeout_secs: 5,
query_timeout_secs: 30,
idle_timeout_secs: 60,
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
client_cert_path: String::new(),
client_key_path: String::new(),
sysdba: false,
oracle_connection_type: None,
connection_string: None,
redis_connection_mode: None,
redis_sentinel_master: String::new(),
redis_sentinel_nodes: String::new(),
redis_sentinel_username: String::new(),
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
external_config: Some(serde_json::json!({ "serverAddr": "http://127.0.0.1:9" })),
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
one_time: false,
read_only: true,
};
state.configs.write().await.insert(cfg.id.clone(), cfg);
let err = nacos_rollback_config_core(
&state,
"nacos-rollback",
NacosConfigRollbackRequest {
namespace: None,
data_id: "app.yaml".to_string(),
group: "DEFAULT_GROUP".to_string(),
history_id: "1".to_string(),
nid: None,
},
)
.await
.unwrap_err();
assert!(err.contains("read-only"));
}
}

View File

@ -0,0 +1,334 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosCapabilities {
pub supports_config_management: bool,
pub supports_config_history: bool,
pub supports_service_management: bool,
pub supports_instance_update: bool,
pub supports_raw_api: bool,
}
impl Default for NacosCapabilities {
fn default() -> Self {
Self {
supports_config_management: true,
supports_config_history: true,
supports_service_management: true,
supports_instance_update: true,
supports_raw_api: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConnectionInfo {
pub server_addr: String,
pub namespace: String,
pub server_version: Option<String>,
pub auth: String,
pub capabilities: NacosCapabilities,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosNamespaceInfo {
pub namespace: String,
pub namespace_show_name: String,
#[serde(default)]
pub namespace_desc: Option<String>,
#[serde(default)]
pub config_count: Option<u64>,
#[serde(default)]
pub quota: Option<u64>,
#[serde(default)]
pub namespace_type: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosNamespaceCreate {
#[serde(default)]
pub namespace_id: Option<String>,
pub namespace_name: String,
#[serde(default)]
pub namespace_desc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosNamespaceUpdate {
pub namespace_id: String,
pub namespace_name: String,
#[serde(default)]
pub namespace_desc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigQuery {
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub group: Option<String>,
#[serde(default)]
pub data_id: Option<String>,
#[serde(default)]
pub search: Option<String>,
#[serde(default)]
pub page_no: Option<u32>,
#[serde(default)]
pub page_size: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigItem {
pub data_id: String,
pub group: String,
pub namespace: String,
#[serde(default)]
pub app_name: Option<String>,
#[serde(default)]
pub desc: Option<String>,
#[serde(default)]
pub tags: Option<String>,
#[serde(default)]
pub config_type: Option<String>,
#[serde(default)]
pub md5: Option<String>,
#[serde(default)]
pub encrypted_data_key: Option<String>,
#[serde(default)]
pub content: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigList {
pub page_no: u32,
pub page_size: u32,
pub total_count: u64,
pub items: Vec<NacosConfigItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigUpsert {
#[serde(default)]
pub namespace: Option<String>,
pub data_id: String,
pub group: String,
pub content: String,
#[serde(default)]
pub config_type: Option<String>,
#[serde(default)]
pub app_name: Option<String>,
#[serde(default)]
pub desc: Option<String>,
#[serde(default)]
pub tags: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigKey {
#[serde(default)]
pub namespace: Option<String>,
pub data_id: String,
pub group: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigHistoryQuery {
#[serde(default)]
pub namespace: Option<String>,
pub data_id: String,
pub group: String,
#[serde(default)]
pub page_no: Option<u32>,
#[serde(default)]
pub page_size: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigHistoryItem {
pub history_id: String,
#[serde(default)]
pub nid: Option<i64>,
pub data_id: String,
pub group: String,
pub namespace: String,
#[serde(default)]
pub app_name: Option<String>,
#[serde(default)]
pub operation: Option<String>,
#[serde(default)]
pub operator: Option<String>,
#[serde(default)]
pub last_modified_time: Option<String>,
#[serde(default)]
pub config_type: Option<String>,
#[serde(default)]
pub tags: Option<String>,
#[serde(default)]
pub md5: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigHistoryList {
pub page_no: u32,
pub page_size: u32,
pub total_count: u64,
pub items: Vec<NacosConfigHistoryItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigHistoryKey {
#[serde(default)]
pub namespace: Option<String>,
pub data_id: String,
pub group: String,
pub history_id: String,
#[serde(default)]
pub nid: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosConfigRollbackRequest {
#[serde(default)]
pub namespace: Option<String>,
pub data_id: String,
pub group: String,
pub history_id: String,
#[serde(default)]
pub nid: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NacosServiceQuery {
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub group_name: Option<String>,
#[serde(default)]
pub service_name: Option<String>,
#[serde(default)]
pub page_no: Option<u32>,
#[serde(default)]
pub page_size: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosServiceInfo {
pub service_name: String,
#[serde(default)]
pub group_name: Option<String>,
#[serde(default)]
pub cluster_count: Option<u64>,
#[serde(default)]
pub ip_count: Option<u64>,
#[serde(default)]
pub healthy_instance_count: Option<u64>,
#[serde(default)]
pub trigger_flag: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosServiceList {
pub page_no: u32,
pub page_size: u32,
pub total_count: u64,
pub items: Vec<NacosServiceInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosInstanceInfo {
pub ip: String,
pub port: u16,
#[serde(default)]
pub service_name: Option<String>,
#[serde(default)]
pub cluster_name: Option<String>,
#[serde(default)]
pub group_name: Option<String>,
#[serde(default)]
pub healthy: Option<bool>,
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub ephemeral: Option<bool>,
#[serde(default)]
pub weight: Option<f64>,
#[serde(default)]
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosInstanceQuery {
#[serde(default)]
pub namespace: Option<String>,
pub service_name: String,
#[serde(default)]
pub group_name: Option<String>,
#[serde(default)]
pub clusters: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosInstanceUpdate {
#[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 healthy: Option<bool>,
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub ephemeral: Option<bool>,
#[serde(default)]
pub weight: Option<f64>,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosRawRequest {
pub method: String,
pub path: String,
#[serde(default)]
pub query: Option<std::collections::HashMap<String, String>>,
#[serde(default)]
pub body: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NacosRawResponse {
pub status: u16,
pub body: serde_json::Value,
#[serde(default)]
pub text: Option<String>,
}

View File

@ -1005,6 +1005,7 @@ pub async fn do_execute(
PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()),
PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()),
PoolKind::MessageQueue => Err("Use Message Queue-specific commands".to_string()),
PoolKind::Nacos => Err("Use Nacos-specific commands".to_string()),
PoolKind::InfluxDb(client) => {
let client = client.clone();
let database = pool_key.split(':').nth(1).unwrap_or("default").to_string();
@ -1757,7 +1758,7 @@ pub async fn execute_statements_in_transaction(
| PoolKind::Turso(_)
| PoolKind::SqlServer(_)
| PoolKind::Agent(_) => TxPath::Explicit,
PoolKind::MessageQueue => TxPath::None,
PoolKind::MessageQueue | PoolKind::Nacos => TxPath::None,
#[cfg(feature = "duckdb-bundled")]
PoolKind::DuckDb(_)
| PoolKind::Redis(_)

View File

@ -335,6 +335,22 @@ async fn main() {
.route("/etcd/get", post(routes::etcd::get))
.route("/etcd/put", post(routes::etcd::put))
.route("/etcd/delete", post(routes::etcd::delete))
// Nacos
.route("/nacos/test-connection", post(routes::nacos::test_connection))
.route("/nacos/namespaces/list", post(routes::nacos::list_namespaces))
.route("/nacos/namespaces/create", post(routes::nacos::create_namespace))
.route("/nacos/namespaces/update", post(routes::nacos::update_namespace))
.route("/nacos/configs/list", post(routes::nacos::list_configs))
.route("/nacos/configs/get", post(routes::nacos::get_config))
.route("/nacos/configs/publish", post(routes::nacos::publish_config))
.route("/nacos/configs/delete", post(routes::nacos::delete_config))
.route("/nacos/configs/history/list", post(routes::nacos::list_config_history))
.route("/nacos/configs/history/get", post(routes::nacos::get_config_history))
.route("/nacos/configs/history/rollback", post(routes::nacos::rollback_config))
.route("/nacos/services/list", post(routes::nacos::list_services))
.route("/nacos/instances/list", post(routes::nacos::list_instances))
.route("/nacos/instances/update", post(routes::nacos::update_instance))
.route("/nacos/raw", post(routes::nacos::raw_request))
// MongoDB
.route("/mongo/list-databases", post(routes::mongo::list_databases))
.route("/mongo/list-collections", post(routes::mongo::list_collections))

View File

@ -102,6 +102,9 @@ pub async fn disconnect_db(
let app = &state.app;
app.remove_connection_pools(&body.connection_id).await;
app.nacos_registry.drop_connection(&body.connection_id).await;
#[cfg(feature = "mq-admin")]
app.mq_registry.drop_connection(&body.connection_id).await;
app.reset_connection_transport(&body.connection_id).await;
if body.connection_id.starts_with("__visible_draft_") {
app.configs.write().await.remove(&body.connection_id);
@ -134,6 +137,7 @@ pub async fn save_connections(
state.app.storage.save_connections(&body.configs).await.map_err(AppError)?;
let sync = sync_connection_configs(&state, &body.configs).await;
remove_connection_pools_for_connection_ids(&state, &sync.connection_pool_ids_to_drop).await;
drop_nacos_adapters_for_connection_ids(&state, &sync.nacos_adapter_ids_to_drop).await;
drop_mq_adapters_for_connection_ids(&state, &sync.mq_adapter_ids_to_drop).await;
Ok(Json(()))
}
@ -142,17 +146,20 @@ pub async fn load_connections(State(state): State<Arc<WebState>>) -> Result<Json
let configs = state.app.storage.load_connections().await.map_err(AppError)?;
let sync = sync_connection_configs(&state, &configs).await;
remove_connection_pools_for_connection_ids(&state, &sync.connection_pool_ids_to_drop).await;
drop_nacos_adapters_for_connection_ids(&state, &sync.nacos_adapter_ids_to_drop).await;
drop_mq_adapters_for_connection_ids(&state, &sync.mq_adapter_ids_to_drop).await;
Ok(Json(configs))
}
struct ConnectionConfigSync {
nacos_adapter_ids_to_drop: Vec<String>,
mq_adapter_ids_to_drop: Vec<String>,
connection_pool_ids_to_drop: Vec<String>,
}
async fn sync_connection_configs(state: &WebState, configs: &[ConnectionConfig]) -> ConnectionConfigSync {
let saved_ids: HashSet<&str> = configs.iter().map(|config| config.id.as_str()).collect();
let mut nacos_adapter_ids_to_drop = HashSet::new();
let mut mq_adapter_ids_to_drop = HashSet::new();
let mut connection_pool_ids_to_drop = HashSet::new();
let mut runtime_configs = state.app.configs.write().await;
@ -161,6 +168,9 @@ async fn sync_connection_configs(state: &WebState, configs: &[ConnectionConfig])
true
} else {
connection_pool_ids_to_drop.insert(id.clone());
if existing.db_type == dbx_core::models::connection::DatabaseType::Nacos {
nacos_adapter_ids_to_drop.insert(id.clone());
}
if existing.db_type == dbx_core::models::connection::DatabaseType::MessageQueue {
mq_adapter_ids_to_drop.insert(id.clone());
}
@ -168,10 +178,16 @@ async fn sync_connection_configs(state: &WebState, configs: &[ConnectionConfig])
}
});
for config in configs {
if config.db_type == dbx_core::models::connection::DatabaseType::Nacos {
nacos_adapter_ids_to_drop.insert(config.id.clone());
}
if config.db_type == dbx_core::models::connection::DatabaseType::MessageQueue {
mq_adapter_ids_to_drop.insert(config.id.clone());
}
if let Some(previous) = runtime_configs.insert(config.id.clone(), config.clone()) {
if previous.db_type == dbx_core::models::connection::DatabaseType::Nacos {
nacos_adapter_ids_to_drop.insert(config.id.clone());
}
if previous.db_type == dbx_core::models::connection::DatabaseType::MessageQueue {
mq_adapter_ids_to_drop.insert(config.id.clone());
}
@ -181,6 +197,7 @@ async fn sync_connection_configs(state: &WebState, configs: &[ConnectionConfig])
}
}
ConnectionConfigSync {
nacos_adapter_ids_to_drop: nacos_adapter_ids_to_drop.into_iter().collect(),
mq_adapter_ids_to_drop: mq_adapter_ids_to_drop.into_iter().collect(),
connection_pool_ids_to_drop: connection_pool_ids_to_drop.into_iter().collect(),
}
@ -190,6 +207,12 @@ fn is_transient_runtime_config_id(id: &str) -> bool {
id.starts_with("__test_") || id.starts_with("__visible_draft_")
}
async fn drop_nacos_adapters_for_connection_ids(state: &WebState, connection_ids: &[String]) {
for connection_id in connection_ids {
state.app.nacos_registry.drop_connection(connection_id).await;
}
}
#[cfg(feature = "mq-admin")]
async fn drop_mq_adapters_for_connection_ids(state: &WebState, connection_ids: &[String]) {
for connection_id in connection_ids {

View File

@ -12,6 +12,7 @@ pub mod layout;
pub mod mongo;
#[cfg(feature = "mq-admin")]
pub mod mq;
pub mod nacos;
pub mod plugins;
pub mod query;
pub mod redis;

View File

@ -0,0 +1,245 @@
use std::sync::Arc;
use axum::extract::State;
use axum::Json;
use crate::error::AppError;
use crate::state::WebState;
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConnReq {
connection_id: String,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NamespaceCreateReq {
connection_id: String,
req: dbx_core::nacos::NacosNamespaceCreate,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NamespaceUpdateReq {
connection_id: String,
req: dbx_core::nacos::NacosNamespaceUpdate,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConfigListReq {
connection_id: String,
query: dbx_core::nacos::NacosConfigQuery,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConfigKeyReq {
connection_id: String,
key: dbx_core::nacos::NacosConfigKey,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConfigPublishReq {
connection_id: String,
req: dbx_core::nacos::NacosConfigUpsert,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConfigHistoryListReq {
connection_id: String,
query: dbx_core::nacos::NacosConfigHistoryQuery,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConfigHistoryKeyReq {
connection_id: String,
key: dbx_core::nacos::NacosConfigHistoryKey,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConfigRollbackReq {
connection_id: String,
req: dbx_core::nacos::NacosConfigRollbackRequest,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ServiceListReq {
connection_id: String,
query: dbx_core::nacos::NacosServiceQuery,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InstanceListReq {
connection_id: String,
query: dbx_core::nacos::NacosInstanceQuery,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InstanceUpdateReq {
connection_id: String,
req: dbx_core::nacos::NacosInstanceUpdate,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RawReq {
connection_id: String,
req: dbx_core::nacos::NacosRawRequest,
}
pub async fn test_connection(
State(state): State<Arc<WebState>>,
Json(req): Json<ConnReq>,
) -> Result<Json<dbx_core::nacos::NacosConnectionInfo>, AppError> {
let result =
dbx_core::nacos::service::nacos_test_connection_core(&state.app, &req.connection_id).await.map_err(AppError)?;
Ok(Json(result))
}
pub async fn list_namespaces(
State(state): State<Arc<WebState>>,
Json(req): Json<ConnReq>,
) -> Result<Json<Vec<dbx_core::nacos::NacosNamespaceInfo>>, AppError> {
let result =
dbx_core::nacos::service::nacos_list_namespaces_core(&state.app, &req.connection_id).await.map_err(AppError)?;
Ok(Json(result))
}
pub async fn create_namespace(
State(state): State<Arc<WebState>>,
Json(req): Json<NamespaceCreateReq>,
) -> Result<Json<()>, AppError> {
dbx_core::nacos::service::nacos_create_namespace_core(&state.app, &req.connection_id, req.req)
.await
.map_err(AppError)?;
Ok(Json(()))
}
pub async fn update_namespace(
State(state): State<Arc<WebState>>,
Json(req): Json<NamespaceUpdateReq>,
) -> Result<Json<()>, AppError> {
dbx_core::nacos::service::nacos_update_namespace_core(&state.app, &req.connection_id, req.req)
.await
.map_err(AppError)?;
Ok(Json(()))
}
pub async fn list_configs(
State(state): State<Arc<WebState>>,
Json(req): Json<ConfigListReq>,
) -> Result<Json<dbx_core::nacos::NacosConfigList>, AppError> {
let result = dbx_core::nacos::service::nacos_list_configs_core(&state.app, &req.connection_id, req.query)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn get_config(
State(state): State<Arc<WebState>>,
Json(req): Json<ConfigKeyReq>,
) -> Result<Json<dbx_core::nacos::NacosConfigItem>, AppError> {
let result = dbx_core::nacos::service::nacos_get_config_core(&state.app, &req.connection_id, req.key)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn publish_config(
State(state): State<Arc<WebState>>,
Json(req): Json<ConfigPublishReq>,
) -> Result<Json<()>, AppError> {
dbx_core::nacos::service::nacos_publish_config_core(&state.app, &req.connection_id, req.req)
.await
.map_err(AppError)?;
Ok(Json(()))
}
pub async fn delete_config(
State(state): State<Arc<WebState>>,
Json(req): Json<ConfigKeyReq>,
) -> Result<Json<()>, AppError> {
dbx_core::nacos::service::nacos_delete_config_core(&state.app, &req.connection_id, req.key)
.await
.map_err(AppError)?;
Ok(Json(()))
}
pub async fn list_config_history(
State(state): State<Arc<WebState>>,
Json(req): Json<ConfigHistoryListReq>,
) -> Result<Json<dbx_core::nacos::NacosConfigHistoryList>, AppError> {
let result = dbx_core::nacos::service::nacos_list_config_history_core(&state.app, &req.connection_id, req.query)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn get_config_history(
State(state): State<Arc<WebState>>,
Json(req): Json<ConfigHistoryKeyReq>,
) -> Result<Json<dbx_core::nacos::NacosConfigItem>, AppError> {
let result = dbx_core::nacos::service::nacos_get_config_history_core(&state.app, &req.connection_id, req.key)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn rollback_config(
State(state): State<Arc<WebState>>,
Json(req): Json<ConfigRollbackReq>,
) -> Result<Json<()>, AppError> {
dbx_core::nacos::service::nacos_rollback_config_core(&state.app, &req.connection_id, req.req)
.await
.map_err(AppError)?;
Ok(Json(()))
}
pub async fn list_services(
State(state): State<Arc<WebState>>,
Json(req): Json<ServiceListReq>,
) -> Result<Json<dbx_core::nacos::NacosServiceList>, AppError> {
let result = dbx_core::nacos::service::nacos_list_services_core(&state.app, &req.connection_id, req.query)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn list_instances(
State(state): State<Arc<WebState>>,
Json(req): Json<InstanceListReq>,
) -> Result<Json<Vec<dbx_core::nacos::NacosInstanceInfo>>, AppError> {
let result = dbx_core::nacos::service::nacos_list_instances_core(&state.app, &req.connection_id, req.query)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn update_instance(
State(state): State<Arc<WebState>>,
Json(req): Json<InstanceUpdateReq>,
) -> Result<Json<()>, AppError> {
dbx_core::nacos::service::nacos_update_instance_core(&state.app, &req.connection_id, req.req)
.await
.map_err(AppError)?;
Ok(Json(()))
}
pub async fn raw_request(
State(state): State<Arc<WebState>>,
Json(req): Json<RawReq>,
) -> Result<Json<dbx_core::nacos::NacosRawResponse>, AppError> {
let result = dbx_core::nacos::service::nacos_raw_request_core(&state.app, &req.connection_id, req.req)
.await
.map_err(AppError)?;
Ok(Json(result))
}

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Nacos">
<defs>
<linearGradient id="nacos-a" x1="10" y1="54" x2="54" y2="10" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#0ea5e9"/>
<stop offset="1" stop-color="#2563eb"/>
</linearGradient>
</defs>
<rect x="6" y="6" width="52" height="52" rx="12" fill="url(#nacos-a)"/>
<path d="M18 43V21h6.4l15.2 22H33.2L24 29.7V43h-6z" fill="#fff"/>
<path d="M40 21h6v22h-6z" fill="#bfdbfe"/>
</svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@ -29,9 +29,13 @@
"@babel/runtime": "^7.29.7",
"@codemirror/autocomplete": "^6.20.2",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.3",
"@codemirror/language": "^6.12.3",
"@codemirror/legacy-modes": "^6.5.3",
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",

View File

@ -109,6 +109,21 @@ test("serializes MQ tabs with selected tenant context", () => {
assert.equal(saved[0]?.mqTenant, "public");
});
test("serializes Nacos admin tabs", () => {
const saved = serializeOpenTabs([
queryTab({
mode: "nacos",
database: "",
nacosNamespace: "dev",
nacosNamespaceName: "Development",
}),
]);
assert.equal(saved[0]?.mode, "nacos");
assert.equal(saved[0]?.nacosNamespace, "dev");
assert.equal(saved[0]?.nacosNamespaceName, "Development");
});
test("serializes evicted result cache handles", () => {
const saved = serializeOpenTabs([
queryTab({

View File

@ -86,6 +86,34 @@ test("MQ tabs with a selected tenant target the matching tenant node", () => {
assert.equal(findSidebarNodeForActiveTab(tab, [flat(tenant)])?.id, "tenant-node");
});
test("Nacos tabs target the matching namespace node", () => {
const tab: QueryTab = {
id: "tab-1",
title: "Nacos:dev",
connectionId: "conn-1",
database: "",
sql: "",
isExecuting: false,
mode: "nacos",
nacosNamespace: "dev",
nacosNamespaceName: "Development",
};
const namespace: TreeNode = {
id: "namespace-node",
label: "Development",
type: "nacos-namespace",
connectionId: "conn-1",
nacosNamespace: "dev",
};
assert.deepEqual(activeTabSidebarTarget(tab), {
type: "nacos-namespace",
connectionId: "conn-1",
namespace: "dev",
});
assert.equal(findSidebarNodeForActiveTab(tab, [flat(namespace)])?.id, "namespace-node");
});
test("saved SQL tabs target the matching visible saved SQL file node", () => {
const tab: QueryTab = {
id: "tab-1",

View File

@ -17,15 +17,27 @@ importers:
'@codemirror/commands':
specifier: ^6.10.3
version: 6.10.3
'@codemirror/lang-html':
specifier: ^6.4.11
version: 6.4.11
'@codemirror/lang-json':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-sql':
specifier: ^6.10.0
version: 6.10.0
'@codemirror/lang-xml':
specifier: ^6.1.0
version: 6.1.0
'@codemirror/lang-yaml':
specifier: ^6.1.3
version: 6.1.3
'@codemirror/language':
specifier: ^6.12.3
version: 6.12.3
'@codemirror/legacy-modes':
specifier: ^6.5.3
version: 6.5.3
'@codemirror/search':
specifier: ^6.7.0
version: 6.7.0
@ -437,15 +449,33 @@ packages:
'@codemirror/commands@6.10.3':
resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==}
'@codemirror/lang-css@6.3.1':
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
'@codemirror/lang-html@6.4.11':
resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
'@codemirror/lang-javascript@6.2.5':
resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==}
'@codemirror/lang-json@6.0.2':
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
'@codemirror/lang-sql@6.10.0':
resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
'@codemirror/lang-xml@6.1.0':
resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==}
'@codemirror/lang-yaml@6.1.3':
resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==}
'@codemirror/language@6.12.3':
resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==}
'@codemirror/legacy-modes@6.5.3':
resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==}
'@codemirror/lint@6.9.5':
resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
@ -905,15 +935,30 @@ packages:
'@lezer/common@1.5.2':
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
'@lezer/css@1.3.3':
resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==}
'@lezer/highlight@1.2.3':
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
'@lezer/html@1.3.13':
resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
'@lezer/javascript@1.5.4':
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
'@lezer/json@1.0.3':
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
'@lezer/lr@1.4.10':
resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
'@lezer/xml@1.0.6':
resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==}
'@lezer/yaml@1.0.4':
resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==}
'@lucide/vue@1.17.0':
resolution: {integrity: sha512-6Q1ZHgr5FbmJzKWe5BxlNdjLj2lbmuH1zwDtVzUJofX0w9UREwKgq4F4jwKqFYyyIS4Rj3FiJvDi2k6djukmmw==}
peerDependencies:
@ -1016,48 +1061,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.53.0':
resolution: {integrity: sha512-I6bhOTroqc3ThrwZ89l2k3ivKuELhdPLbAcJhRNyjWvlgwb0vjRgEnVL1XLx5Jud04/ypNRZBykAWrSk6l/D+g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.53.0':
resolution: {integrity: sha512-w0p3JzB/PkkQjXALMJMqP9YfP3yq4w6zGsu5kezQmUnxRkN3b/Theg2l/nDgBsOcczxS3gL6Gam5XNAVrO6QJQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.53.0':
resolution: {integrity: sha512-mzBhF6k1Yq1K/dqDmVe/AAafnlJfEpx7yfUiksyeWXJk5iSzZqBSxcsa02zIytYgQFRZ7h6WPZfwHg/DoOE1Kw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.53.0':
resolution: {integrity: sha512-AlFCpnRQhogQFzZXWbO6xB6/Udy745L+eQNmDPGg7G/OeWsYmJc4jZYfUN5pQg0reOPWSED2mOQqKZOJM1U8cA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.53.0':
resolution: {integrity: sha512-XD4ulY4f1DWbuuZXAqxhVn+gdPmrhnmojWtFN78ctVoupmS845fGhsUrk1HZXKQI+iymbaiz9vAjPsghHNQ7Ag==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.53.0':
resolution: {integrity: sha512-xg8KWX0QnxmYWRe60CgHYWXI0ZOtBbqTsXvWiWrcl2XUHJ3fht2QerOk2iWvylzX3zNT2GpvBRxGoR4d3sxPRQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.53.0':
resolution: {integrity: sha512-MWExpYBGvl+pIvVB/gj/CcWlN2al8AizT7rUbtaYaWNoQkhWARM6W3qpgoCr72CYSN9PborzPmM5MIRe2BrNdA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.53.0':
resolution: {integrity: sha512-u4sajgO4nxgmJIgc/y2AqPhkdbOkQH8WugXpA1+pW0ESQhvGZ1oGq61Q4xMbJHJU1hFgtO18QNrcFYDPYH0gwQ==}
@ -1130,48 +1183,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.68.0':
resolution: {integrity: sha512-qVKtCZNic+OoNnOr/hCQAu22HSQzflI7Fsq/Blzkw02SnLuv163k3kfmrVpZjSBlUHgsRKj6WgQiw30d3SX02Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.68.0':
resolution: {integrity: sha512-zExyZ8ZOUuAyQ0y9jpTcyjKUz62YY9JhKPyVxzvjTpXzZ3ujdqiVwfPWDdnA1SsIOrxdtxHn7KErDHLWskFjXg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.68.0':
resolution: {integrity: sha512-6C4MPuwewyDavA7sxM14wzgRi5GGL68HPIxRCdVyS75U4MDbpFVYzKO9WNR6KLKTMPq2pcz3THwo1sK2uiqngw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.68.0':
resolution: {integrity: sha512-bnZooVeHAcvA+dH0EDLgx+7HY/DRi6e0hFszg3P+OBatuUjV6EvfIyNIzWOusmqAVh4L6r21GGTZtiKE4iqM4Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.68.0':
resolution: {integrity: sha512-dIqnZnJSmHCMOUpUcWQOiV14o3DDPVx1DSsMaSzvdhNjC1tB1iEPZbdiMSCIEYbkgbsYznHXWqFdKL8WUB3F8g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.68.0':
resolution: {integrity: sha512-zc9lEnfV/HreDTY6gdMlZe+irkwHSxQ4/B1pS9GyK7RVaA5LxhoZY/w6/o2vIwLLEYiXQ5ujGxOM1ZazeFAAIA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.68.0':
resolution: {integrity: sha512-Dl5QEX0TCo/40Cdh1o1JdPS//+YiWqjC+Hrrya5OQmStZZr4svAFtdlqcpCrU9yq2Mo3vRVyO9B3h0dzD8s36Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.68.0':
resolution: {integrity: sha512-/qy6dOvi4S3/LeXq0l5BT5pRKPYA7oj3uKwJOAZOr5HRLL+HK6jdBynvWuXIA2wwfE01RzNYmbBdM7vwYx00sA==}
@ -1232,36 +1293,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.3':
resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.3':
resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.3':
resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.3':
resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.3':
resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.3':
resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==}
@ -1364,24 +1431,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.3.0':
resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.3.0':
resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.3.0':
resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.3.0':
resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==}
@ -1450,30 +1521,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.11.2':
resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.11.2':
resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.11.2':
resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.11.2':
resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.11.2':
resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==}
@ -2721,24 +2797,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@ -4153,6 +4233,36 @@ snapshots:
'@codemirror/view': 6.43.0
'@lezer/common': 1.5.2
'@codemirror/lang-css@6.3.1':
dependencies:
'@codemirror/autocomplete': 6.20.2
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@lezer/common': 1.5.2
'@lezer/css': 1.3.3
'@codemirror/lang-html@6.4.11':
dependencies:
'@codemirror/autocomplete': 6.20.2
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-javascript': 6.2.5
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@codemirror/view': 6.43.0
'@lezer/common': 1.5.2
'@lezer/css': 1.3.3
'@lezer/html': 1.3.13
'@codemirror/lang-javascript@6.2.5':
dependencies:
'@codemirror/autocomplete': 6.20.2
'@codemirror/language': 6.12.3
'@codemirror/lint': 6.9.5
'@codemirror/state': 6.6.0
'@codemirror/view': 6.43.0
'@lezer/common': 1.5.2
'@lezer/javascript': 1.5.4
'@codemirror/lang-json@6.0.2':
dependencies:
'@codemirror/language': 6.12.3
@ -4167,6 +4277,25 @@ snapshots:
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@codemirror/lang-xml@6.1.0':
dependencies:
'@codemirror/autocomplete': 6.20.2
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@codemirror/view': 6.43.0
'@lezer/common': 1.5.2
'@lezer/xml': 1.0.6
'@codemirror/lang-yaml@6.1.3':
dependencies:
'@codemirror/autocomplete': 6.20.2
'@codemirror/language': 6.12.3
'@codemirror/state': 6.6.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/yaml': 1.0.4
'@codemirror/language@6.12.3':
dependencies:
'@codemirror/state': 6.6.0
@ -4176,6 +4305,10 @@ snapshots:
'@lezer/lr': 1.4.10
style-mod: 4.1.3
'@codemirror/legacy-modes@6.5.3':
dependencies:
'@codemirror/language': 6.12.3
'@codemirror/lint@6.9.5':
dependencies:
'@codemirror/state': 6.6.0
@ -4514,10 +4647,28 @@ snapshots:
'@lezer/common@1.5.2': {}
'@lezer/css@1.3.3':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/highlight@1.2.3':
dependencies:
'@lezer/common': 1.5.2
'@lezer/html@1.3.13':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/javascript@1.5.4':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.5.2
@ -4528,6 +4679,18 @@ snapshots:
dependencies:
'@lezer/common': 1.5.2
'@lezer/xml@1.0.6':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/yaml@1.0.4':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lucide/vue@1.17.0(vue@3.5.35(typescript@6.0.3))':
dependencies:
vue: 3.5.35(typescript@6.0.3)

View File

@ -433,17 +433,20 @@ async fn save_connection_configs(state: &AppState, configs: &[ConnectionConfig])
state.storage.save_connections(configs).await?;
let sync = sync_connection_configs(state, configs).await;
remove_connection_pools_for_connection_ids(state, &sync.connection_pool_ids_to_drop).await;
drop_nacos_adapters_for_connection_ids(state, &sync.nacos_adapter_ids_to_drop).await;
drop_mq_adapters_for_connection_ids(state, &sync.mq_adapter_ids_to_drop).await;
Ok(())
}
struct ConnectionConfigSync {
nacos_adapter_ids_to_drop: Vec<String>,
mq_adapter_ids_to_drop: Vec<String>,
connection_pool_ids_to_drop: Vec<String>,
}
async fn sync_connection_configs(state: &AppState, configs: &[ConnectionConfig]) -> ConnectionConfigSync {
let saved_ids: HashSet<&str> = configs.iter().map(|config| config.id.as_str()).collect();
let mut nacos_adapter_ids_to_drop = HashSet::new();
let mut mq_adapter_ids_to_drop = HashSet::new();
let mut connection_pool_ids_to_drop = HashSet::new();
let mut runtime_configs = state.configs.write().await;
@ -452,6 +455,9 @@ async fn sync_connection_configs(state: &AppState, configs: &[ConnectionConfig])
true
} else {
connection_pool_ids_to_drop.insert(id.clone());
if existing.db_type == DatabaseType::Nacos {
nacos_adapter_ids_to_drop.insert(id.clone());
}
if existing.db_type == DatabaseType::MessageQueue {
mq_adapter_ids_to_drop.insert(id.clone());
}
@ -459,10 +465,16 @@ async fn sync_connection_configs(state: &AppState, configs: &[ConnectionConfig])
}
});
for config in configs {
if config.db_type == DatabaseType::Nacos {
nacos_adapter_ids_to_drop.insert(config.id.clone());
}
if config.db_type == DatabaseType::MessageQueue {
mq_adapter_ids_to_drop.insert(config.id.clone());
}
if let Some(previous) = runtime_configs.insert(config.id.clone(), config.clone()) {
if previous.db_type == DatabaseType::Nacos {
nacos_adapter_ids_to_drop.insert(config.id.clone());
}
if previous.db_type == DatabaseType::MessageQueue {
mq_adapter_ids_to_drop.insert(config.id.clone());
}
@ -472,6 +484,7 @@ async fn sync_connection_configs(state: &AppState, configs: &[ConnectionConfig])
}
}
ConnectionConfigSync {
nacos_adapter_ids_to_drop: nacos_adapter_ids_to_drop.into_iter().collect(),
mq_adapter_ids_to_drop: mq_adapter_ids_to_drop.into_iter().collect(),
connection_pool_ids_to_drop: connection_pool_ids_to_drop.into_iter().collect(),
}
@ -481,6 +494,12 @@ fn is_transient_runtime_config_id(id: &str) -> bool {
id.starts_with("__test_") || id.starts_with("__visible_draft_")
}
async fn drop_nacos_adapters_for_connection_ids(state: &AppState, connection_ids: &[String]) {
for connection_id in connection_ids {
state.nacos_registry.drop_connection(connection_id).await;
}
}
#[cfg(feature = "mq-admin")]
async fn drop_mq_adapters_for_connection_ids(state: &AppState, connection_ids: &[String]) {
for connection_id in connection_ids {
@ -507,6 +526,7 @@ async fn load_connection_configs(state: &AppState) -> Result<Vec<ConnectionConfi
state.storage.load_connections().await?.into_iter().map(|config| config.canonicalized()).collect();
let sync = sync_connection_configs(state, &configs).await;
remove_connection_pools_for_connection_ids(state, &sync.connection_pool_ids_to_drop).await;
drop_nacos_adapters_for_connection_ids(state, &sync.nacos_adapter_ids_to_drop).await;
drop_mq_adapters_for_connection_ids(state, &sync.mq_adapter_ids_to_drop).await;
Ok(configs)
}
@ -762,6 +782,12 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
.await
.map(|_| "Connection successful".to_string())
}
DatabaseType::Nacos => {
let admin_config = state.nacos_admin_config_for_connection(connection_id, &config).await?;
let adapter = state.nacos_registry.build_transient_config(admin_config).await?;
adapter.test_connection().await?;
Ok("Connection successful".to_string())
}
#[cfg(feature = "mq-admin")]
DatabaseType::MessageQueue => {
let mqc = state.mq_admin_config_for_connection(connection_id, &config).await?;
@ -1028,6 +1054,12 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
db::influxdb_driver::test_connection(&client, connect_timeout).await?;
PoolKind::InfluxDb(client)
}
DatabaseType::Nacos => {
let admin_config = state.nacos_admin_config_for_connection(&id, &config).await?;
let adapter = state.nacos_registry.build_transient_config(admin_config).await?;
adapter.test_connection().await?;
PoolKind::Nacos
}
#[cfg(feature = "mq-admin")]
DatabaseType::MessageQueue => {
let mqc = state.mq_admin_config_for_connection(&id, &config).await?;
@ -1080,6 +1112,7 @@ pub async fn connection_final_proxy_port(
#[tauri::command]
pub async fn disconnect_db(state: State<'_, Arc<AppState>>, connection_id: String) -> Result<(), String> {
state.remove_connection_pools(&connection_id).await;
drop_nacos_adapters_for_connection_ids(state.inner(), std::slice::from_ref(&connection_id)).await;
drop_mq_adapters_for_connection_ids(state.inner(), std::slice::from_ref(&connection_id)).await;
state.reset_connection_transport(&connection_id).await;
if connection_id.starts_with("__visible_draft_") {

View File

@ -20,6 +20,7 @@ pub mod mcp_bridge;
pub mod mongo_cmd;
#[cfg(feature = "mq-admin")]
pub mod mq_cmd;
pub mod nacos_cmd;
pub mod plugins;
pub mod query;
pub mod query_cancel;

View File

@ -0,0 +1,138 @@
use std::sync::Arc;
use tauri::State;
use crate::commands::connection::AppState;
#[tauri::command]
pub async fn nacos_test_connection(
state: State<'_, Arc<AppState>>,
connection_id: String,
) -> Result<dbx_core::nacos::NacosConnectionInfo, String> {
dbx_core::nacos::service::nacos_test_connection_core(&state, &connection_id).await
}
#[tauri::command]
pub async fn nacos_list_namespaces(
state: State<'_, Arc<AppState>>,
connection_id: String,
) -> Result<Vec<dbx_core::nacos::NacosNamespaceInfo>, String> {
dbx_core::nacos::service::nacos_list_namespaces_core(&state, &connection_id).await
}
#[tauri::command]
pub async fn nacos_create_namespace(
state: State<'_, Arc<AppState>>,
connection_id: String,
req: dbx_core::nacos::NacosNamespaceCreate,
) -> Result<(), String> {
dbx_core::nacos::service::nacos_create_namespace_core(&state, &connection_id, req).await
}
#[tauri::command]
pub async fn nacos_update_namespace(
state: State<'_, Arc<AppState>>,
connection_id: String,
req: dbx_core::nacos::NacosNamespaceUpdate,
) -> Result<(), String> {
dbx_core::nacos::service::nacos_update_namespace_core(&state, &connection_id, req).await
}
#[tauri::command]
pub async fn nacos_list_configs(
state: State<'_, Arc<AppState>>,
connection_id: String,
query: dbx_core::nacos::NacosConfigQuery,
) -> Result<dbx_core::nacos::NacosConfigList, String> {
dbx_core::nacos::service::nacos_list_configs_core(&state, &connection_id, query).await
}
#[tauri::command]
pub async fn nacos_get_config(
state: State<'_, Arc<AppState>>,
connection_id: String,
key: dbx_core::nacos::NacosConfigKey,
) -> Result<dbx_core::nacos::NacosConfigItem, String> {
dbx_core::nacos::service::nacos_get_config_core(&state, &connection_id, key).await
}
#[tauri::command]
pub async fn nacos_publish_config(
state: State<'_, Arc<AppState>>,
connection_id: String,
req: dbx_core::nacos::NacosConfigUpsert,
) -> Result<(), String> {
dbx_core::nacos::service::nacos_publish_config_core(&state, &connection_id, req).await
}
#[tauri::command]
pub async fn nacos_delete_config(
state: State<'_, Arc<AppState>>,
connection_id: String,
key: dbx_core::nacos::NacosConfigKey,
) -> Result<(), String> {
dbx_core::nacos::service::nacos_delete_config_core(&state, &connection_id, key).await
}
#[tauri::command]
pub async fn nacos_list_config_history(
state: State<'_, Arc<AppState>>,
connection_id: String,
query: dbx_core::nacos::NacosConfigHistoryQuery,
) -> Result<dbx_core::nacos::NacosConfigHistoryList, String> {
dbx_core::nacos::service::nacos_list_config_history_core(&state, &connection_id, query).await
}
#[tauri::command]
pub async fn nacos_get_config_history(
state: State<'_, Arc<AppState>>,
connection_id: String,
key: dbx_core::nacos::NacosConfigHistoryKey,
) -> Result<dbx_core::nacos::NacosConfigItem, String> {
dbx_core::nacos::service::nacos_get_config_history_core(&state, &connection_id, key).await
}
#[tauri::command]
pub async fn nacos_rollback_config(
state: State<'_, Arc<AppState>>,
connection_id: String,
req: dbx_core::nacos::NacosConfigRollbackRequest,
) -> Result<(), String> {
dbx_core::nacos::service::nacos_rollback_config_core(&state, &connection_id, req).await
}
#[tauri::command]
pub async fn nacos_list_services(
state: State<'_, Arc<AppState>>,
connection_id: String,
query: dbx_core::nacos::NacosServiceQuery,
) -> Result<dbx_core::nacos::NacosServiceList, String> {
dbx_core::nacos::service::nacos_list_services_core(&state, &connection_id, query).await
}
#[tauri::command]
pub async fn nacos_list_instances(
state: State<'_, Arc<AppState>>,
connection_id: String,
query: dbx_core::nacos::NacosInstanceQuery,
) -> Result<Vec<dbx_core::nacos::NacosInstanceInfo>, String> {
dbx_core::nacos::service::nacos_list_instances_core(&state, &connection_id, query).await
}
#[tauri::command]
pub async fn nacos_update_instance(
state: State<'_, Arc<AppState>>,
connection_id: String,
req: dbx_core::nacos::NacosInstanceUpdate,
) -> Result<(), String> {
dbx_core::nacos::service::nacos_update_instance_core(&state, &connection_id, req).await
}
#[tauri::command]
pub async fn nacos_raw_request(
state: State<'_, Arc<AppState>>,
connection_id: String,
req: dbx_core::nacos::NacosRawRequest,
) -> Result<dbx_core::nacos::NacosRawResponse, String> {
dbx_core::nacos::service::nacos_raw_request_core(&state, &connection_id, req).await
}

View File

@ -572,6 +572,21 @@ pub fn run() {
commands::etcd_cmd::etcd_get,
commands::etcd_cmd::etcd_put,
commands::etcd_cmd::etcd_delete,
commands::nacos_cmd::nacos_test_connection,
commands::nacos_cmd::nacos_list_namespaces,
commands::nacos_cmd::nacos_create_namespace,
commands::nacos_cmd::nacos_update_namespace,
commands::nacos_cmd::nacos_list_configs,
commands::nacos_cmd::nacos_get_config,
commands::nacos_cmd::nacos_publish_config,
commands::nacos_cmd::nacos_delete_config,
commands::nacos_cmd::nacos_list_config_history,
commands::nacos_cmd::nacos_get_config_history,
commands::nacos_cmd::nacos_rollback_config,
commands::nacos_cmd::nacos_list_services,
commands::nacos_cmd::nacos_list_instances,
commands::nacos_cmd::nacos_update_instance,
commands::nacos_cmd::nacos_raw_request,
commands::saved_sql::load_saved_sql_library,
commands::saved_sql::save_saved_sql_folder,
commands::saved_sql::delete_saved_sql_folder,