feat(nacos): add r-nacos compatibility
This commit is contained in:
31 files changed
+2481
-168
No files matched your search
@@ -16,7 +16,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import type { ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo, DatabaseType, HttpTunnelConfig, IdentifierCase, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshConfigHostEntry, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
|
||||
import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb";
|
||||
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
|
||||
import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos";
|
||||
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
|
||||
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
|
||||
@@ -53,6 +53,7 @@ import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses";
|
||||
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
|
||||
import { driverInstallProgressChannel, driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||
import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
|
||||
import { normalizeNacosEndpoint } from "@/lib/nacos/nacosAdmin";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowDown,
|
||||
@@ -114,9 +115,6 @@ const DREMIO_ARROW_FLIGHT_SQL_JDBC_URL = "jdbc:arrow-flight-sql://127.0.0.1:3201
|
||||
const DREMIO_ARROW_FLIGHT_SQL_JDBC_DRIVER_CLASS = "org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver";
|
||||
const DREMIO_LEGACY_JDBC_URL = "jdbc:dremio:direct=127.0.0.1:31010";
|
||||
const DREMIO_LEGACY_JDBC_DRIVER_CLASS = "com.dremio.jdbc.Driver";
|
||||
const NACOS_DEFAULT_CONSOLE_URL = "http://127.0.0.1:8085";
|
||||
const NACOS_LEGACY_SERVER_PORT = "8848";
|
||||
const NACOS_DOCKER_CONSOLE_PORT = "8085";
|
||||
const DEFAULT_SSH_USER = "root";
|
||||
|
||||
type LegacyTransportFields = {
|
||||
@@ -586,14 +584,77 @@ const mqKafkaSaslMechanismOptions = [
|
||||
{ value: "SCRAM-SHA-256", label: "SCRAM-SHA-256" },
|
||||
{ value: "SCRAM-SHA-512", label: "SCRAM-SHA-512" },
|
||||
];
|
||||
const nacosServerAddr = ref(NACOS_DEFAULT_CONSOLE_URL);
|
||||
const nacosImplementation = ref<NacosImplementation>("nacos");
|
||||
const nacosVersionMode = ref<NacosVersionMode>("auto");
|
||||
const nacosServerAddr = ref("");
|
||||
const nacosNamespace = ref("");
|
||||
const nacosContextPath = ref("");
|
||||
const nacosContextPathCustomized = ref(false);
|
||||
const nacosRNacosConsoleAddr = ref("");
|
||||
const nacosHistoryEnabled = ref(false);
|
||||
const nacosConsoleAuthKind = ref<NacosRNacosConsoleAuth["kind"]>("inherit");
|
||||
const nacosConsoleUsername = ref("");
|
||||
const nacosConsolePassword = ref("");
|
||||
const nacosAuthKind = ref<NacosAuthKind>("none");
|
||||
const nacosUsername = ref("nacos");
|
||||
const nacosPassword = ref("");
|
||||
const nacosTlsSkipVerify = ref(false);
|
||||
const nacosPageSize = ref(20);
|
||||
const nacosPrimaryAddressLabel = computed(() => {
|
||||
if (nacosImplementation.value === "rnacos") return t("connection.nacosPrimaryAddressRNacos");
|
||||
if (nacosVersionMode.value === "v2") return t("connection.nacosPrimaryAddressV2");
|
||||
if (nacosVersionMode.value === "v3") return t("connection.nacosPrimaryAddressV3");
|
||||
return t("connection.nacosPrimaryAddressAuto");
|
||||
});
|
||||
const nacosPrimaryAddressPlaceholder = computed(() => {
|
||||
if (nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2") return "http://127.0.0.1:8848/nacos";
|
||||
return "http://127.0.0.1:8080";
|
||||
});
|
||||
const nacosNormalizedPreview = computed(() => {
|
||||
if (!nacosServerAddr.value.trim()) return "";
|
||||
try {
|
||||
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
return `${normalized.serverAddr}${normalized.contextPath || ""}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
const nacosEffectiveContextPath = computed(() => {
|
||||
if (!nacosServerAddr.value.trim()) {
|
||||
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2" ? "/nacos" : "/";
|
||||
}
|
||||
try {
|
||||
const normalized = normalizeNacosEndpoint(nacosServerAddr.value, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
return normalized.contextPath || "/";
|
||||
} catch {
|
||||
return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : "/";
|
||||
}
|
||||
});
|
||||
const nacosContextPathInput = computed({
|
||||
get: () => (nacosContextPathCustomized.value ? nacosContextPath.value : nacosEffectiveContextPath.value),
|
||||
set: (value: string) => {
|
||||
nacosContextPathCustomized.value = true;
|
||||
nacosContextPath.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
function resetNacosContextPathCustomization() {
|
||||
nacosContextPathCustomized.value = false;
|
||||
nacosContextPath.value = "";
|
||||
}
|
||||
|
||||
watch(nacosImplementation, (implementation) => {
|
||||
if (implementation === "rnacos") nacosVersionMode.value = "auto";
|
||||
if (implementation !== "rnacos") nacosHistoryEnabled.value = false;
|
||||
});
|
||||
|
||||
const colorOptions = [
|
||||
{ value: "", class: "bg-transparent border-dashed", labelKey: "connection.colorNone" },
|
||||
@@ -1002,9 +1063,18 @@ watch(mqAuthKind, (kind) => {
|
||||
});
|
||||
|
||||
function resetNacosFields(config?: Partial<NacosAdminConfig>) {
|
||||
nacosServerAddr.value = config?.serverAddr?.trim() || NACOS_DEFAULT_CONSOLE_URL;
|
||||
nacosImplementation.value = config?.implementation || (config?.rnacosConsoleAddr ? "rnacos" : "nacos");
|
||||
nacosVersionMode.value = config?.versionMode || "auto";
|
||||
nacosServerAddr.value = config?.serverAddr?.trim() || "";
|
||||
nacosNamespace.value = config?.namespace || "";
|
||||
nacosContextPath.value = config?.contextPath || "";
|
||||
nacosContextPathCustomized.value = !!config?.contextPath;
|
||||
nacosRNacosConsoleAddr.value = config?.rnacosConsoleAddr?.trim() || "";
|
||||
nacosHistoryEnabled.value = config?.rnacosHistoryEnabled ?? !!config?.rnacosConsoleAddr;
|
||||
const consoleAuth = config?.rnacosConsoleAuth || { kind: "inherit" };
|
||||
nacosConsoleAuthKind.value = consoleAuth.kind;
|
||||
nacosConsoleUsername.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.username : "";
|
||||
nacosConsolePassword.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.password : "";
|
||||
nacosTlsSkipVerify.value = !!config?.tlsSkipVerify;
|
||||
nacosPageSize.value = Number(config?.pageSize) > 0 ? Number(config?.pageSize) : 20;
|
||||
const auth = (config?.auth || { kind: "none" }) as NacosAuthConfig;
|
||||
@@ -1200,10 +1270,38 @@ function buildNacosAuth(): NacosAuthConfig {
|
||||
}
|
||||
|
||||
function buildNacosAdminConfig(): NacosAdminConfig {
|
||||
const primaryAddress = requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired"));
|
||||
const normalized = normalizeNacosEndpoint(primaryAddress, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined,
|
||||
});
|
||||
if (nacosImplementation.value === "rnacos" && normalized.warnings.length) {
|
||||
throw new Error(t("connection.nacosRNacosOpenApiRequired"));
|
||||
}
|
||||
let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined;
|
||||
if (nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value) {
|
||||
if (!nacosRNacosConsoleAddr.value.trim()) throw new Error(t("connection.nacosRNacosConsoleUrlRequired"));
|
||||
if (nacosConsoleAuthKind.value === "inherit") {
|
||||
if (nacosAuthKind.value !== "usernamePassword") throw new Error(t("connection.nacosConsoleAuthSeparateRequired"));
|
||||
rnacosConsoleAuth = { kind: "inherit" };
|
||||
} else {
|
||||
rnacosConsoleAuth = {
|
||||
kind: "usernamePassword",
|
||||
username: requireMqField(nacosConsoleUsername.value, t("connection.nacosConsoleUsernameRequired")),
|
||||
password: nacosConsolePassword.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
serverAddr: requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired")),
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosImplementation.value === "nacos" ? nacosVersionMode.value : undefined,
|
||||
serverAddr: normalized.serverAddr,
|
||||
namespace: nacosNamespace.value.trim() || undefined,
|
||||
contextPath: nacosContextPath.value.trim(),
|
||||
contextPath: normalized.contextPath || undefined,
|
||||
rnacosConsoleAddr: nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value ? nacosRNacosConsoleAddr.value.trim() || undefined : undefined,
|
||||
rnacosHistoryEnabled: nacosImplementation.value === "rnacos" ? nacosHistoryEnabled.value : undefined,
|
||||
rnacosConsoleAuth,
|
||||
auth: buildNacosAuth(),
|
||||
tlsSkipVerify: nacosTlsSkipVerify.value || undefined,
|
||||
pageSize: Number(nacosPageSize.value) > 0 ? Number(nacosPageSize.value) : 20,
|
||||
@@ -1219,10 +1317,9 @@ function dockerNacosConsoleFallbackUrl(serverAddr: string): string | 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;
|
||||
if (port !== "8848" || !["127.0.0.1", "localhost", "::1"].includes(host)) return null;
|
||||
|
||||
parsed.port = "8085";
|
||||
return parsed.toString().replace(/\/$/, "");
|
||||
}
|
||||
|
||||
@@ -1232,6 +1329,7 @@ function isNacosAdminEndpointNotFound(message: string): boolean {
|
||||
|
||||
async function tryNacosDockerConsoleFallback(config: ConnectionConfig, originalError: string, runId: number): Promise<SuccessfulConnectionTest | null> {
|
||||
if (config.db_type !== "nacos" || !isNacosAdminEndpointNotFound(originalError)) return null;
|
||||
|
||||
const fallbackUrl = dockerNacosConsoleFallbackUrl(nacosServerAddr.value);
|
||||
if (!fallbackUrl || fallbackUrl === nacosServerAddr.value.trim()) return null;
|
||||
|
||||
@@ -1404,6 +1502,11 @@ async function setSqlServerDriverMode(mode: "auto" | "legacy") {
|
||||
}
|
||||
}
|
||||
|
||||
function isSqlServerTlsHandshakeFailure(message: string): boolean {
|
||||
const text = message.toLowerCase();
|
||||
return text.includes("sql server") && text.includes("tls") && (text.includes("handshake") || text.includes("eof") || text.includes("performing i/o"));
|
||||
}
|
||||
|
||||
function clearTestedConnectionInfo() {
|
||||
testedConfigFingerprint.value = "";
|
||||
testedConfigId.value = "";
|
||||
@@ -2661,6 +2764,10 @@ async function testConnection() {
|
||||
void persistSuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName, runId);
|
||||
clearEditedConnectionErrorAfterSuccessfulTest();
|
||||
} else {
|
||||
const shouldShowSqlServerLegacyMode = config?.db_type === "sqlserver" && !sqlServerUsesLegacyCompatibility(config) && isSqlServerTlsHandshakeFailure(message);
|
||||
if (shouldShowSqlServerLegacyMode) {
|
||||
configTab.value = "advanced";
|
||||
}
|
||||
clearTestedConnectionInfo();
|
||||
testResult.value = { ok: false, message };
|
||||
showConnectionError(message);
|
||||
@@ -4456,7 +4563,7 @@ function openExternalUrl(url: string) {
|
||||
|
||||
<TabsContent value="connection" class="m-0 min-h-0 flex-1 overflow-hidden">
|
||||
<div class="connection-form-body grid h-full min-h-0 gap-4 overflow-y-auto pt-4 pr-2">
|
||||
<div v-if="!isJdbcConnection" class="grid grid-cols-4 items-center gap-4">
|
||||
<div v-if="!isJdbcConnection && form.db_type !== 'nacos'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.connectionUrlOptional") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
<Input v-model="connectionUrlInput" class="flex-1" :placeholder="connectionUrlPlaceholder" @keydown.enter.prevent="applyConnectionUrl" />
|
||||
@@ -5018,24 +5125,92 @@ function openExternalUrl(url: string) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Nacos: server address, namespace and auth -->
|
||||
<!-- Nacos: profile-aware endpoint, namespace and auth -->
|
||||
<template v-else-if="form.db_type === 'nacos'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleUrl") }}</Label>
|
||||
<Input v-model="nacosServerAddr" class="col-span-3" placeholder="http://127.0.0.1:8085" />
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosImplementation") }}</Label>
|
||||
<div class="col-span-3 flex gap-2">
|
||||
<Button size="sm" :variant="nacosImplementation === 'nacos' ? 'default' : 'outline'" @click="nacosImplementation = 'nacos'">Nacos</Button>
|
||||
<Button size="sm" :variant="nacosImplementation === 'rnacos' ? 'default' : 'outline'" @click="nacosImplementation = 'rnacos'">r-nacos</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nacosImplementation === 'nacos'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosVersion") }}</Label>
|
||||
<div class="col-span-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" :variant="nacosVersionMode === 'auto' ? 'default' : 'outline'" @click="nacosVersionMode = 'auto'">{{ t("connection.nacosVersionAuto") }}</Button>
|
||||
<Button size="sm" :variant="nacosVersionMode === 'v2' ? 'default' : 'outline'" @click="nacosVersionMode = 'v2'">2.x</Button>
|
||||
<Button size="sm" :variant="nacosVersionMode === 'v3' ? 'default' : 'outline'" @click="nacosVersionMode = 'v3'">3.x</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ nacosPrimaryAddressLabel }}</Label>
|
||||
<Input v-model="nacosServerAddr" class="col-span-3" :placeholder="nacosPrimaryAddressPlaceholder" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosConsoleUrlHint") }}</p>
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosPrimaryAddressHint") }}</p>
|
||||
</div>
|
||||
<div v-if="nacosNormalizedPreview" class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs leading-5 text-muted-foreground">{{ t("connection.nacosRequestsUse", { address: nacosNormalizedPreview }) }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosContextPath") }}</Label>
|
||||
<div class="col-span-3 flex min-w-0 items-center gap-2">
|
||||
<Input v-model="nacosContextPathInput" class="min-w-0 flex-1" :placeholder="t('connection.nacosContextPathPlaceholder')" />
|
||||
<Button v-if="nacosContextPathCustomized" type="button" size="sm" variant="ghost" @click="resetNacosContextPathCustomization">{{ t("connection.nacosContextPathRestoreAuto") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosNamespace") }}</Label>
|
||||
<Input v-model="nacosNamespace" class="col-span-3" placeholder="public" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosContextPath") }}</Label>
|
||||
<Input v-model="nacosContextPath" class="col-span-3" :placeholder="t('connection.nacosContextPathPlaceholder')" />
|
||||
</div>
|
||||
<template v-if="nacosImplementation === 'rnacos'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConfigurationHistory") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-2">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<Switch v-model="nacosHistoryEnabled" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.nacosConfigurationHistoryEnable") }}</span>
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<CircleHelp class="h-3.5 w-3.5 cursor-help text-muted-foreground hover:text-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" class="max-w-[360px] text-xs leading-relaxed">
|
||||
{{ t("connection.nacosConfigurationHistoryHint") }}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="nacosHistoryEnabled">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosRNacosConsoleUrl") }}</Label>
|
||||
<Input v-model="nacosRNacosConsoleAddr" class="col-span-3" :placeholder="t('connection.nacosRNacosConsoleUrlPlaceholder')" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleAuthentication") }}</Label>
|
||||
<div class="col-span-3 flex gap-2">
|
||||
<Button size="sm" :variant="nacosConsoleAuthKind === 'inherit' ? 'default' : 'outline'" :disabled="nacosAuthKind === 'none'" @click="nacosConsoleAuthKind = 'inherit'">{{ t("connection.nacosConsoleAuthInherit") }}</Button>
|
||||
<Button size="sm" :variant="nacosConsoleAuthKind === 'usernamePassword' ? 'default' : 'outline'" @click="nacosConsoleAuthKind = 'usernamePassword'">{{ t("connection.nacosConsoleAuthSeparate") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nacosConsoleAuthKind === 'inherit' && nacosAuthKind === 'none'" class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 m-0 text-xs text-destructive">{{ t("connection.nacosConsoleAuthPrimaryNone") }}</p>
|
||||
</div>
|
||||
<template v-if="nacosConsoleAuthKind === 'usernamePassword'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsoleUser") }}</Label
|
||||
><Input v-model="nacosConsoleUsername" class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosConsolePassword") }}</Label
|
||||
><PasswordInput v-model="nacosConsolePassword" class="col-span-3" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.nacosAuth") }}</Label>
|
||||
<div class="col-span-3 flex flex-wrap gap-2">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
|
||||
import NacosConfigDiffDialog from "@/components/nacos/NacosConfigDiffDialog.vue";
|
||||
@@ -85,6 +86,12 @@ const historyCompareLoading = ref(false);
|
||||
const historyCompareItem = ref<NacosConfigHistoryItem | null>(null);
|
||||
const pendingHistoryRollback = ref<NacosConfigHistoryItem | null>(null);
|
||||
const rollingBackHistory = ref(false);
|
||||
const rnacosConsoleAuthOpen = ref(false);
|
||||
const rnacosConsoleCaptchaImage = ref("");
|
||||
const rnacosConsoleCaptcha = ref("");
|
||||
const rnacosConsoleAuthError = ref("");
|
||||
const rnacosConsoleAuthLoading = ref(false);
|
||||
const rnacosConsoleRetryAction = shallowRef<(() => Promise<void>) | null>(null);
|
||||
const configFormatOptions = ["text", "json", "xml", "yaml", "html", "properties", "toml"];
|
||||
const configEditorHost = ref<HTMLDivElement | null>(null);
|
||||
const configEditorView = shallowRef<EditorView | null>(null);
|
||||
@@ -117,6 +124,15 @@ const CONNECTION_NOT_FOUND_RETRY_DELAYS_MS = [150, 350, 700];
|
||||
const { gridTemplateColumns: configListGridTemplate, minWidth: configListMinWidth, resizingColumnIndex: configListResizingColumnIndex, onResizeStart: onConfigListColumnResizeStart } = useNacosConfigListColumnResize();
|
||||
|
||||
const namespace = computed(() => props.namespace ?? connectionInfo.value?.namespace ?? "");
|
||||
const supportsConfigHistory = computed(() => connectionInfo.value?.capabilities.supportsConfigHistory !== false);
|
||||
const configHistoryUnavailableTitle = computed(() => {
|
||||
if (supportsConfigHistory.value) return undefined;
|
||||
const reason = connectionInfo.value?.capabilities.historyUnavailableReason;
|
||||
if (reason === "historyDisabled") return t("nacos.historyDisabled");
|
||||
if (reason === "consoleUrlMissing") return t("nacos.historyConsoleUrlMissing");
|
||||
if (reason === "consoleCredentialsMissing") return t("nacos.historyConsoleCredentialsMissing");
|
||||
return t("nacos.historyUnavailable");
|
||||
});
|
||||
const namespaceLabel = computed(() => props.namespaceName || namespace.value || "public");
|
||||
const namespaceIdLabel = computed(() => {
|
||||
if (!namespace.value || namespace.value === namespaceLabel.value) return "";
|
||||
@@ -537,7 +553,7 @@ function historyKeyFor(item: NacosConfigHistoryItem) {
|
||||
}
|
||||
|
||||
async function openConfigHistory() {
|
||||
if (!selectedConfigOriginalKey.value || !selectedConfig.value) return;
|
||||
if (!selectedConfigOriginalKey.value || !selectedConfig.value || !supportsConfigHistory.value) return;
|
||||
historyOpen.value = true;
|
||||
await loadConfigHistory(1);
|
||||
}
|
||||
@@ -556,17 +572,81 @@ async function loadConfigHistory(page = historyPageNo.value) {
|
||||
historyItems.value = result.items;
|
||||
historyTotal.value = result.totalCount;
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => loadConfigHistory(historyPageNo.value));
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRNacosHistoryError(error: unknown, retryAction: () => Promise<void>) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (!message.includes("[rnacosConsoleCaptchaRequired]")) {
|
||||
historyError.value = message;
|
||||
return false;
|
||||
}
|
||||
rnacosConsoleRetryAction.value = retryAction;
|
||||
await requestRNacosConsoleAuthentication();
|
||||
return true;
|
||||
}
|
||||
|
||||
function retryRNacosConsoleAction() {
|
||||
const retryAction = rnacosConsoleRetryAction.value;
|
||||
rnacosConsoleRetryAction.value = null;
|
||||
// Run after the failed action has finished its own catch/finally cleanup;
|
||||
// otherwise that stale cleanup can close or clear the successfully retried UI.
|
||||
setTimeout(() => void (retryAction ? retryAction() : loadConfigHistory(historyPageNo.value)), 0);
|
||||
}
|
||||
|
||||
function rnacosCaptchaImageSource(image: string) {
|
||||
return image.startsWith("data:") ? image : `data:image/png;base64,${image}`;
|
||||
}
|
||||
|
||||
async function requestRNacosConsoleAuthentication() {
|
||||
rnacosConsoleAuthError.value = "";
|
||||
rnacosConsoleCaptcha.value = "";
|
||||
rnacosConsoleAuthLoading.value = true;
|
||||
try {
|
||||
const challenge = await api.nacosGetRNacosConsoleCaptcha(props.connectionId);
|
||||
if (!challenge.required) {
|
||||
await api.nacosLoginRNacosConsole(props.connectionId);
|
||||
void loadInfo();
|
||||
retryRNacosConsoleAction();
|
||||
return;
|
||||
}
|
||||
if (!challenge.image) throw new Error(t("nacos.rnacosCaptchaUnavailable"));
|
||||
rnacosConsoleCaptchaImage.value = rnacosCaptchaImageSource(challenge.image);
|
||||
rnacosConsoleAuthOpen.value = true;
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
rnacosConsoleAuthLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRNacosConsoleAuthentication() {
|
||||
if (!rnacosConsoleCaptcha.value.trim()) {
|
||||
rnacosConsoleAuthError.value = t("nacos.rnacosCaptchaRequired");
|
||||
return;
|
||||
}
|
||||
rnacosConsoleAuthLoading.value = true;
|
||||
rnacosConsoleAuthError.value = "";
|
||||
try {
|
||||
await api.nacosLoginRNacosConsole(props.connectionId, rnacosConsoleCaptcha.value);
|
||||
rnacosConsoleAuthOpen.value = false;
|
||||
void loadInfo();
|
||||
retryRNacosConsoleAction();
|
||||
} catch (error) {
|
||||
rnacosConsoleAuthError.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
rnacosConsoleAuthLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistoryDetail(item: NacosConfigHistoryItem): Promise<NacosConfigItem | null> {
|
||||
try {
|
||||
return await api.nacosGetConfigHistory(props.connectionId, historyKeyFor(item));
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => viewConfigHistory(item));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -600,7 +680,7 @@ async function compareConfigHistory(item: NacosConfigHistoryItem) {
|
||||
historyCompareCurrent.value = current.content || "";
|
||||
historyCompareContent.value = history.content || "";
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => compareConfigHistory(item));
|
||||
historyCompareOpen.value = false;
|
||||
} finally {
|
||||
historyCompareLoading.value = false;
|
||||
@@ -636,7 +716,7 @@ async function rollbackConfigHistory() {
|
||||
}
|
||||
await Promise.all([loadConfigs(configPageNo.value), loadConfigHistory(historyPageNo.value)]);
|
||||
} catch (error) {
|
||||
historyError.value = error instanceof Error ? error.message : String(error);
|
||||
await handleRNacosHistoryError(error, () => rollbackConfigHistory());
|
||||
} finally {
|
||||
rollingBackHistory.value = false;
|
||||
}
|
||||
@@ -1028,10 +1108,12 @@ onBeforeUnmount(() => {
|
||||
<Download class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.export") }}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="!selectedConfigOriginalKey" @click="openConfigHistory">
|
||||
<FileClock class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.history") }}
|
||||
</Button>
|
||||
<span class="inline-flex" :title="configHistoryUnavailableTitle">
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="!selectedConfigOriginalKey || !supportsConfigHistory" @click="openConfigHistory">
|
||||
<FileClock class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.history") }}
|
||||
</Button>
|
||||
</span>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="readOnly" @click="saveConfigAsCopy">
|
||||
<Save class="h-3.5 w-3.5" />
|
||||
{{ t("nacos.saveAs") }}
|
||||
@@ -1188,6 +1270,30 @@ onBeforeUnmount(() => {
|
||||
@rollback="requestRollbackHistory"
|
||||
/>
|
||||
|
||||
<Dialog v-model:open="rnacosConsoleAuthOpen">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.rnacosConsoleAuthTitle") }}</DialogTitle>
|
||||
<DialogDescription>{{ t("nacos.rnacosConsoleAuthDescription") }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3">
|
||||
<img v-if="rnacosConsoleCaptchaImage" :src="rnacosConsoleCaptchaImage" :alt="t('nacos.rnacosCaptchaLabel')" class="h-28 w-full rounded-md border bg-muted/30 object-contain" />
|
||||
<div class="space-y-1.5">
|
||||
<Label for="rnacos-console-captcha">{{ t("nacos.rnacosCaptchaLabel") }}</Label>
|
||||
<Input id="rnacos-console-captcha" v-model="rnacosConsoleCaptcha" autocomplete="off" :placeholder="t('nacos.rnacosCaptchaPlaceholder')" @keyup.enter="submitRNacosConsoleAuthentication" />
|
||||
</div>
|
||||
<p v-if="rnacosConsoleAuthError" class="text-xs text-destructive">{{ rnacosConsoleAuthError }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="rnacosConsoleAuthLoading" @click="requestRNacosConsoleAuthentication">{{ t("nacos.rnacosRefreshCaptcha") }}</Button>
|
||||
<Button :disabled="rnacosConsoleAuthLoading" @click="submitRNacosConsoleAuthentication">
|
||||
<Loader2 v-if="rnacosConsoleAuthLoading" class="mr-2 h-4 w-4 animate-spin" />
|
||||
{{ t("nacos.rnacosConsoleAuthSubmit") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<NacosConfigDiffDialog
|
||||
v-model:open="historyCompareOpen"
|
||||
:title="t('nacos.historyCompareTitle')"
|
||||
|
||||
Reference in new issue
Block a user