feat(nacos): add Nacos management console (#1456)
This commit is contained in:
parent
ff1559f018
commit
3ebfae05a2
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ const assetIcons: Record<string, string> = {
|
|||
milvus: "milvus.png",
|
||||
mq: "pulsar",
|
||||
pulsar: "pulsar",
|
||||
nacos: "nacos.png",
|
||||
iris: "iris.png",
|
||||
influxdb: "influxdb",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: "読み取り専用",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: "只读",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: "唯讀",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1843,3 +1843,4 @@ export async function exportQueryResultMarkdown(filePath: string, columns: strin
|
|||
}
|
||||
|
||||
export * from "./mq-tauri";
|
||||
export * from "./nacos-tauri";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)?))
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
}
|
||||
|
|
@ -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(_)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
163
pnpm-lock.yaml
163
pnpm-lock.yaml
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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_") {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue