feat(nacos): add access control and namespace administration
This commit is contained in:
59 files changed
+4563
-479
No files matched your search
@@ -19,7 +19,7 @@ import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb";
|
||||
import type { VictoriaMetricsExternalConfig } from "@/types/victoriametrics";
|
||||
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
|
||||
import type { MqttConnectionConfig } from "@/types/mqtt";
|
||||
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosNamespaceInfo, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
|
||||
import type { NacosAdminConfig, NacosApiPlane, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosNamespaceInfo, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
|
||||
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||
import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles";
|
||||
@@ -298,6 +298,11 @@ const visibleNacosNamespaces = ref<NacosNamespaceInfo[]>([]);
|
||||
const visibleNacosNamespaceSelection = ref<Set<string>>(new Set());
|
||||
const visibleNacosNamespaceSearchText = ref("");
|
||||
const visibleNacosNamespaceError = ref("");
|
||||
const visibleNacosNamespaceListingPermissionDenied = ref(false);
|
||||
const isResolvingManualNacosNamespaces = ref(false);
|
||||
const visibleNacosNamespaceAccessMode = ref<"automatic" | "manual">("automatic");
|
||||
const nacosDynamicAllNamespaces = ref(false);
|
||||
const visibleNacosNamespaceDynamicAllSupported = ref(false);
|
||||
const showProductionDatabasesDialog = ref(false);
|
||||
const isLoadingProductionDatabases = ref(false);
|
||||
const productionDatabaseNames = ref<string[]>([]);
|
||||
@@ -883,8 +888,9 @@ const nacosImplementation = ref<NacosImplementation>("nacos");
|
||||
// Nacos 2 and 3 expose different API planes. New connections must therefore
|
||||
// choose an explicit version instead of relying on endpoint-shape guessing.
|
||||
const nacosVersionMode = ref<NacosVersionMode>("v2");
|
||||
const nacosApiPlane = ref<NacosApiPlane>("admin");
|
||||
const nacosServerAddr = ref("");
|
||||
const nacosOrdinaryAccount = ref(false);
|
||||
const nacosContextPath = ref("");
|
||||
const nacosManagedNamespacesText = ref("");
|
||||
const nacosRNacosConsoleAddr = ref("");
|
||||
const nacosHistoryEnabled = ref(false);
|
||||
@@ -898,6 +904,11 @@ const nacosTlsSkipVerify = ref(false);
|
||||
const nacosMetricsMode = ref<NacosMetricsMode>("auto");
|
||||
const nacosMetricsUrl = ref("");
|
||||
const nacosPageSize = ref(20);
|
||||
let nacosScopeFingerprintBaseline = "";
|
||||
|
||||
function currentNacosScopeFingerprint(): string {
|
||||
return JSON.stringify([nacosImplementation.value, nacosVersionMode.value, nacosApiPlane.value, nacosServerAddr.value.trim().replace(/\/+$/, ""), nacosContextPath.value.trim(), nacosAuthKind.value, nacosUsername.value.trim()]);
|
||||
}
|
||||
type ConsulConsistency = "default" | "stale" | "consistent";
|
||||
const consulServerAddr = ref("http://127.0.0.1:8500");
|
||||
const consulDatacenter = ref("");
|
||||
@@ -935,15 +946,25 @@ const mqttConnectTimeoutSecs = ref(30);
|
||||
const mqttMaxPacketSizeBytes = ref(16 * 1024 * 1024);
|
||||
const mqttSavedTopics = ref<MqttConnectionConfig["savedTopics"]>([]);
|
||||
const nacosPrimaryAddressPlaceholder = computed(() => {
|
||||
if (nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "console") {
|
||||
return "http://127.0.0.1:8080";
|
||||
}
|
||||
return "http://127.0.0.1:8848/nacos";
|
||||
});
|
||||
const nacosServiceAddressHint = computed(() => {
|
||||
if (nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "console") {
|
||||
return t("nacos.nacosConsoleServiceAddressHint");
|
||||
}
|
||||
return t("nacos.nacosServiceAddressHint");
|
||||
});
|
||||
const nacosV3AdminEndpointWarning = computed(() => {
|
||||
if (nacosImplementation.value !== "nacos" || nacosVersionMode.value !== "v3" || !nacosServerAddr.value.trim()) return "";
|
||||
try {
|
||||
const url = new URL(nacosServerAddr.value.trim());
|
||||
if (url.port === "8080") {
|
||||
if (nacosApiPlane.value === "admin" && url.port === "8080") {
|
||||
return t("nacos.nacosV3ConsolePortWarning");
|
||||
}
|
||||
if (nacosApiPlane.value === "console" && url.port === "8848") return t("nacos.nacosV3AdminPortWarning");
|
||||
} catch {
|
||||
// The normal form validation will report an invalid URL on save.
|
||||
}
|
||||
@@ -971,6 +992,7 @@ function selectNacosConnectionProfile(profile: NacosConnectionProfile) {
|
||||
}
|
||||
nacosImplementation.value = "nacos";
|
||||
nacosVersionMode.value = profile;
|
||||
if (profile !== "v3") nacosApiPlane.value = "admin";
|
||||
}
|
||||
|
||||
watch(nacosImplementation, (implementation) => {
|
||||
@@ -1459,11 +1481,13 @@ function resetNacosFields(config?: Partial<NacosAdminConfig>) {
|
||||
// Saved `auto` profiles are legacy records. The form always saves an
|
||||
// explicit selection and no longer relies on a separate Console address.
|
||||
nacosVersionMode.value = config?.versionMode === "v3" ? "v3" : "v2";
|
||||
nacosApiPlane.value = config?.versionMode === "v3" ? config?.apiPlane || "admin" : "admin";
|
||||
const serverAddr = config?.serverAddr?.trim() || "";
|
||||
const contextPath = config?.contextPath?.trim() || "";
|
||||
nacosServerAddr.value = serverAddr && contextPath && contextPath !== "/" && !serverAddr.endsWith(contextPath) ? `${serverAddr.replace(/\/+$/, "")}/${contextPath.replace(/^\/+/, "")}` : serverAddr;
|
||||
nacosOrdinaryAccount.value = !!config?.managedNamespaces?.length;
|
||||
nacosServerAddr.value = serverAddr;
|
||||
nacosContextPath.value = contextPath;
|
||||
nacosManagedNamespacesText.value = (config?.managedNamespaces || []).join("\n");
|
||||
nacosDynamicAllNamespaces.value = !!config && !config.managedNamespaces?.length && !Array.isArray(form.value.visible_databases);
|
||||
nacosRNacosConsoleAddr.value = config?.rnacosConsoleAddr?.trim() || "";
|
||||
nacosHistoryEnabled.value = config?.rnacosHistoryEnabled ?? !!config?.rnacosConsoleAddr;
|
||||
const consoleAuth = config?.rnacosConsoleAuth || { kind: "inherit" };
|
||||
@@ -1478,6 +1502,7 @@ function resetNacosFields(config?: Partial<NacosAdminConfig>) {
|
||||
nacosAuthKind.value = auth.kind || "none";
|
||||
nacosUsername.value = auth.username || "nacos";
|
||||
nacosPassword.value = auth.password || "";
|
||||
nacosScopeFingerprintBaseline = currentNacosScopeFingerprint();
|
||||
}
|
||||
|
||||
function hydrateNacosFields(value: unknown) {
|
||||
@@ -1805,7 +1830,8 @@ function buildNacosAdminConfig(): NacosAdminConfig {
|
||||
const normalized = normalizeNacosEndpoint(primaryAddress, {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosVersionMode.value,
|
||||
contextPath: undefined,
|
||||
apiPlane: nacosApiPlane.value,
|
||||
contextPath: nacosContextPath.value,
|
||||
});
|
||||
if (nacosImplementation.value === "rnacos" && normalized.warnings.length) {
|
||||
throw new Error(t("connection.nacosRNacosOpenApiRequired"));
|
||||
@@ -1815,11 +1841,7 @@ function buildNacosAdminConfig(): NacosAdminConfig {
|
||||
throw new Error(t("connection.nacosRNacosConsoleUrlRequired"));
|
||||
}
|
||||
let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined;
|
||||
const usesManagedNamespaces = nacosImplementation.value === "nacos" && nacosAuthKind.value === "usernamePassword" && nacosOrdinaryAccount.value;
|
||||
const managedNamespaces = usesManagedNamespaces ? parseNacosManagedNamespaces(nacosManagedNamespacesText.value) : [];
|
||||
if (usesManagedNamespaces && managedNamespaces.length === 0) {
|
||||
throw new Error(t("nacos.nacosOrdinaryNamespacesRequired"));
|
||||
}
|
||||
const managedNamespaces = nacosImplementation.value === "nacos" && nacosAuthKind.value === "usernamePassword" ? parseNacosManagedNamespaces(nacosManagedNamespacesText.value) : [];
|
||||
let metricsUrl: string | undefined;
|
||||
if (nacosMetricsMode.value === "custom") {
|
||||
try {
|
||||
@@ -1843,6 +1865,7 @@ function buildNacosAdminConfig(): NacosAdminConfig {
|
||||
return {
|
||||
implementation: nacosImplementation.value,
|
||||
versionMode: nacosImplementation.value === "nacos" ? nacosVersionMode.value : undefined,
|
||||
apiPlane: nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" ? nacosApiPlane.value : undefined,
|
||||
serverAddr: normalized.serverAddr,
|
||||
contextPath: normalized.contextPath || undefined,
|
||||
managedNamespaces: managedNamespaces.length ? managedNamespaces : undefined,
|
||||
@@ -3461,6 +3484,24 @@ const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHi
|
||||
const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true);
|
||||
const agentDriverFocus = computed<DriverStoreFocus>(() => ({ target: "driver", driver: agentDriverInstallKey(form.value.db_type, form.value.driver_profile) }));
|
||||
const canChooseVisibleNacosNamespaces = computed(() => form.value.db_type === "nacos");
|
||||
const isNacosV3AdminPlane = computed(() => nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "admin");
|
||||
const isNacosV3ConsolePlane = computed(() => nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "console");
|
||||
const canDetectNacosNamespaceAccess = computed(() => form.value.db_type === "nacos" && nacosImplementation.value === "nacos" && nacosAuthKind.value === "usernamePassword");
|
||||
const nacosManualNamespaceLabelKey = computed(() => (isNacosV3AdminPlane.value ? "nacos.nacosManagedNamespaces" : "nacos.nacosManagedNamespacesNameOrId"));
|
||||
const nacosManualNamespacePlaceholderKey = computed(() => (isNacosV3AdminPlane.value ? "nacos.nacosManagedNamespacesIdPlaceholder" : "nacos.nacosManagedNamespacesPlaceholder"));
|
||||
const nacosManualNamespaceHintKey = computed(() => {
|
||||
if (isNacosV3AdminPlane.value) return "nacos.nacosV3AdminManagedNamespacesHint";
|
||||
if (isNacosV3ConsolePlane.value) return "nacos.nacosV3ConsoleManagedNamespacesHint";
|
||||
return "nacos.nacosManagedNamespacesHint";
|
||||
});
|
||||
const nacosNamespacePickerTitleKey = computed(() => (canDetectNacosNamespaceAccess.value ? "nacos.nacosDetectAccessibleNamespaces" : "nacos.nacosVisibleNamespacesTitle"));
|
||||
const nacosNamespacePickerDescriptionKey = computed(() => (canDetectNacosNamespaceAccess.value ? "nacos.nacosDetectAccessibleNamespacesHint" : "nacos.nacosVisibleNamespacesDescription"));
|
||||
function hasNacosNamespaceScopeForSave(): boolean {
|
||||
if (!canDetectNacosNamespaceAccess.value) return true;
|
||||
if (nacosDynamicAllNamespaces.value) return true;
|
||||
if (parseNacosManagedNamespaces(nacosManagedNamespacesText.value).length > 0) return true;
|
||||
return Array.isArray(form.value.visible_databases) && form.value.visible_databases.length > 0;
|
||||
}
|
||||
const canChooseVisibleDatabases = computed(() => !canChooseVisibleNacosNamespaces.value && connectionCanChooseVisibleDatabases(form.value));
|
||||
const visibleFilterUsesSchemas = computed(() => connectionUsesVisibleSchemaFilter(form.value));
|
||||
const hasVisibleDatabaseFilter = computed(() => Array.isArray(form.value.visible_databases));
|
||||
@@ -3494,7 +3535,7 @@ const filteredVisibleNacosNamespaces = computed(() => {
|
||||
});
|
||||
});
|
||||
const visibleNacosNamespaceSelectedCount = computed(() => visibleNacosNamespaceSelection.value.size);
|
||||
const visibleNacosNamespaceCanSave = computed(() => visibleNacosNamespaceSelection.value.size > 0);
|
||||
const visibleNacosNamespaceCanSave = computed(() => (visibleNacosNamespaceAccessMode.value === "manual" ? parseNacosManagedNamespaces(nacosManagedNamespacesText.value).length > 0 : visibleNacosNamespaceSelection.value.size > 0));
|
||||
const filteredProductionDatabaseNames = computed(() => {
|
||||
const query = productionDatabaseSearchText.value.trim().toLowerCase();
|
||||
if (!query) return productionDatabaseNames.value;
|
||||
@@ -4632,6 +4673,10 @@ function resetVisibleNacosNamespaceDraftState() {
|
||||
visibleNacosNamespaceSelection.value = new Set();
|
||||
visibleNacosNamespaceSearchText.value = "";
|
||||
visibleNacosNamespaceError.value = "";
|
||||
visibleNacosNamespaceListingPermissionDenied.value = false;
|
||||
isResolvingManualNacosNamespaces.value = false;
|
||||
visibleNacosNamespaceAccessMode.value = "automatic";
|
||||
visibleNacosNamespaceDynamicAllSupported.value = false;
|
||||
}
|
||||
|
||||
function resetProductionDatabaseDraftState() {
|
||||
@@ -4715,24 +4760,99 @@ function normalizeVisibleNacosNamespaceSelection(selected: Iterable<string>, nam
|
||||
return normalizeNacosNamespaceSelection(selected, namespaces);
|
||||
}
|
||||
|
||||
function normalizeManualNacosNamespaceNames(namespaces: string[], availableNamespaces: NacosNamespaceInfo[]): string[] {
|
||||
const namespaceIds = new Map(availableNamespaces.map((namespace) => [nacosNamespaceIdentity(namespace.namespace), namespace.namespace]));
|
||||
const namespaceNames = new Map<string, string[]>();
|
||||
for (const namespace of availableNamespaces) {
|
||||
const name = nacosNamespaceLabel(namespace).trim();
|
||||
if (!name) continue;
|
||||
namespaceNames.set(name, [...(namespaceNames.get(name) || []), namespace.namespace]);
|
||||
}
|
||||
|
||||
const normalized: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const namespace of namespaces) {
|
||||
const namespaceId = namespaceIds.get(nacosNamespaceIdentity(namespace));
|
||||
const matchingNames = namespaceNames.get(namespace.trim()) || [];
|
||||
// Display names are only safe to convert when the server returned one
|
||||
// exact match. Keep unknown or ambiguous values intact so the regular
|
||||
// permission check can report them instead of targeting a wrong namespace.
|
||||
const value = namespaceId ?? (matchingNames.length === 1 ? matchingNames[0] : namespace);
|
||||
const identity = nacosNamespaceIdentity(value);
|
||||
if (!seen.has(identity)) {
|
||||
seen.add(identity);
|
||||
normalized.push(value);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function resolveManualNacosNamespaceNames(namespaces: string[]): Promise<string[]> {
|
||||
const draftId = buildDraftVisibleDatabasesConnectionId(uuid());
|
||||
try {
|
||||
const submittedConfig = connectionConfigForSubmit(draftId);
|
||||
const draftConfig = {
|
||||
...submittedConfig,
|
||||
// Resolve display names against the authenticated directory rather than
|
||||
// passing them as namespace IDs to the configuration and naming APIs.
|
||||
external_config: submittedConfig.db_type === "nacos" ? { ...(submittedConfig.external_config as NacosAdminConfig), managedNamespaces: undefined } : submittedConfig.external_config,
|
||||
id: draftId,
|
||||
one_time: true,
|
||||
};
|
||||
await api.connectDb(draftConfig);
|
||||
return normalizeManualNacosNamespaceNames(namespaces, await loadReadableNacosNamespaces(draftId, api));
|
||||
} catch {
|
||||
// Some Nacos deployments do not expose a readable namespace directory to
|
||||
// ordinary users. Preserve manually entered IDs for the normal test path.
|
||||
return namespaces;
|
||||
} finally {
|
||||
await api.disconnectDb(draftId).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function isNacosNamespaceListingPermissionError(error: unknown): boolean {
|
||||
const message = errorMessage(error);
|
||||
if (/NACOS_ERROR\[(?:v3ManagedNamespacesRequired|managedNamespacesRequired)\]/.test(message)) return true;
|
||||
return /\/v3\/(?:admin|console)\/core\/namespace\/list/.test(message) && /\b403\b/.test(message) && /authorization failed/i.test(message);
|
||||
}
|
||||
|
||||
async function openVisibleNacosNamespacesPicker() {
|
||||
if (!ensureConnectionHostResolvedFromUrl()) return;
|
||||
if (!canChooseVisibleNacosNamespaces.value || isLoadingVisibleNacosNamespaces.value) return;
|
||||
|
||||
isLoadingVisibleNacosNamespaces.value = true;
|
||||
visibleNacosNamespaceError.value = "";
|
||||
visibleNacosNamespaceListingPermissionDenied.value = false;
|
||||
visibleNacosNamespaceDynamicAllSupported.value = false;
|
||||
visibleNacosNamespaceSearchText.value = "";
|
||||
visibleNacosNamespaceAccessMode.value = "automatic";
|
||||
const draftId = buildDraftVisibleDatabasesConnectionId(uuid());
|
||||
|
||||
try {
|
||||
const submittedConfig = connectionConfigForSubmit(draftId);
|
||||
const draftConfig = {
|
||||
...connectionConfigForSubmit(draftId),
|
||||
...submittedConfig,
|
||||
// Automatic detection must not inherit a previously saved manual scope.
|
||||
// Otherwise it would only rediscover that old subset instead of checking
|
||||
// what the currently configured account can really access.
|
||||
external_config: canDetectNacosNamespaceAccess.value && submittedConfig.db_type === "nacos" ? { ...(submittedConfig.external_config as NacosAdminConfig), managedNamespaces: undefined } : submittedConfig.external_config,
|
||||
visible_databases: undefined,
|
||||
id: draftId,
|
||||
one_time: true,
|
||||
};
|
||||
await api.connectDb(draftConfig);
|
||||
const namespaces = await loadReadableNacosNamespaces(draftId, api);
|
||||
visibleNacosNamespaces.value = [...namespaces].sort((left, right) => nacosNamespaceLabel(left).localeCompare(nacosNamespaceLabel(right)));
|
||||
try {
|
||||
const sidebarSnapshot = await api.nacosSidebarSnapshot(draftId);
|
||||
const readableIds = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaces.value.map(nacosNamespaceValue), visibleNacosNamespaces.value);
|
||||
const sidebarIds = normalizeVisibleNacosNamespaceSelection(sidebarSnapshot.namespaces.map(nacosNamespaceValue), visibleNacosNamespaces.value);
|
||||
const readableSet = new Set(readableIds.map(nacosNamespaceIdentity));
|
||||
const sidebarSet = new Set(sidebarIds.map(nacosNamespaceIdentity));
|
||||
visibleNacosNamespaceDynamicAllSupported.value = readableSet.size === sidebarSet.size && [...readableSet].every((namespace) => sidebarSet.has(namespace));
|
||||
} catch {
|
||||
visibleNacosNamespaceDynamicAllSupported.value = false;
|
||||
}
|
||||
const configured = form.value.visible_databases;
|
||||
const initialSelection = Array.isArray(configured) ? normalizeVisibleNacosNamespaceSelection(configured, visibleNacosNamespaces.value) : visibleNacosNamespaces.value.map(nacosNamespaceValue);
|
||||
visibleNacosNamespaceSelection.value = new Set(initialSelection);
|
||||
@@ -4740,7 +4860,8 @@ async function openVisibleNacosNamespacesPicker() {
|
||||
} catch (e: any) {
|
||||
visibleNacosNamespaces.value = [];
|
||||
visibleNacosNamespaceSelection.value = new Set();
|
||||
visibleNacosNamespaceError.value = errorMessage(e);
|
||||
visibleNacosNamespaceListingPermissionDenied.value = isNacosNamespaceListingPermissionError(e);
|
||||
visibleNacosNamespaceError.value = visibleNacosNamespaceListingPermissionDenied.value ? t("nacos.nacosManagedNamespacesRequired") : errorMessage(e);
|
||||
testResult.value = { ok: false, message: visibleNacosNamespaceError.value };
|
||||
showVisibleNacosNamespacesDialog.value = true;
|
||||
} finally {
|
||||
@@ -4765,13 +4886,31 @@ function clearVisibleNacosNamespaceSelection() {
|
||||
}
|
||||
|
||||
function showAllVisibleNacosNamespaces() {
|
||||
nacosDynamicAllNamespaces.value = true;
|
||||
nacosManagedNamespacesText.value = "";
|
||||
form.value.visible_databases = undefined;
|
||||
resetVisibleNacosNamespaceDraftState();
|
||||
}
|
||||
|
||||
function saveVisibleNacosNamespaceSelection() {
|
||||
async function saveVisibleNacosNamespaceSelection() {
|
||||
if (!visibleNacosNamespaceCanSave.value) return;
|
||||
form.value.visible_databases = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaceSelection.value, visibleNacosNamespaces.value);
|
||||
if (visibleNacosNamespaceAccessMode.value === "manual") {
|
||||
const manualNamespaces = parseNacosManagedNamespaces(nacosManagedNamespacesText.value);
|
||||
isResolvingManualNacosNamespaces.value = true;
|
||||
const resolvedNamespaces = isNacosV3AdminPlane.value ? manualNamespaces : await resolveManualNacosNamespaceNames(manualNamespaces);
|
||||
isResolvingManualNacosNamespaces.value = false;
|
||||
nacosManagedNamespacesText.value = resolvedNamespaces.join("\n");
|
||||
form.value.visible_databases = resolvedNamespaces;
|
||||
nacosDynamicAllNamespaces.value = false;
|
||||
} else {
|
||||
nacosManagedNamespacesText.value = "";
|
||||
const selected = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaceSelection.value, visibleNacosNamespaces.value);
|
||||
const all = normalizeVisibleNacosNamespaceSelection(visibleNacosNamespaces.value.map(nacosNamespaceValue), visibleNacosNamespaces.value);
|
||||
const selectsEntireReadableList = selected.length === all.length && selected.every((namespace, index) => namespace === all[index]);
|
||||
const useDynamicAll = selectsEntireReadableList && visibleNacosNamespaceDynamicAllSupported.value;
|
||||
form.value.visible_databases = useDynamicAll ? undefined : selected;
|
||||
nacosDynamicAllNamespaces.value = useDynamicAll;
|
||||
}
|
||||
showVisibleNacosNamespacesDialog.value = false;
|
||||
}
|
||||
|
||||
@@ -5214,6 +5353,26 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => currentNacosScopeFingerprint(),
|
||||
(current) => {
|
||||
if (form.value.db_type !== "nacos") {
|
||||
nacosScopeFingerprintBaseline = current;
|
||||
return;
|
||||
}
|
||||
if (!nacosScopeFingerprintBaseline || current === nacosScopeFingerprintBaseline) {
|
||||
nacosScopeFingerprintBaseline = current;
|
||||
return;
|
||||
}
|
||||
nacosScopeFingerprintBaseline = current;
|
||||
nacosManagedNamespacesText.value = "";
|
||||
nacosDynamicAllNamespaces.value = false;
|
||||
form.value.visible_databases = undefined;
|
||||
resetVisibleNacosNamespaceDraftState();
|
||||
resetTestState();
|
||||
},
|
||||
);
|
||||
|
||||
watch(visibleDatabaseShowSystem, (show) => {
|
||||
if (show) return;
|
||||
const connection = connectionConfigSnapshotForVisibleDatabases();
|
||||
@@ -5404,6 +5563,11 @@ async function persistConnectionNoteVisibilityDraft() {
|
||||
async function save() {
|
||||
if (!ensureConnectionHostResolvedFromUrl()) return;
|
||||
if (isSaving.value) return;
|
||||
if (!hasNacosNamespaceScopeForSave()) {
|
||||
testResult.value = null;
|
||||
await openVisibleNacosNamespacesPicker();
|
||||
return;
|
||||
}
|
||||
const databaseInfoForSave = visibleTestDatabaseInfo.value ?? visibleSavedDatabaseInfo.value;
|
||||
isSaving.value = true;
|
||||
let connectionSaved = false;
|
||||
@@ -6619,11 +6783,28 @@ function openExternalUrl(url: string) {
|
||||
|
||||
<section data-nacos-endpoint-section class="rounded-lg border p-4">
|
||||
<div class="grid gap-4">
|
||||
<div v-if="nacosImplementation === 'nacos' && nacosVersionMode === 'v3'" class="grid gap-1.5">
|
||||
<Label>{{ t("nacos.nacosApiPlane") }}</Label>
|
||||
<div class="grid grid-cols-2 gap-1 rounded-md border bg-muted/20 p-1">
|
||||
<button
|
||||
v-for="plane in ['admin', 'console'] as NacosApiPlane[]"
|
||||
:key="plane"
|
||||
type="button"
|
||||
class="min-w-0 rounded px-3 py-2 text-left transition-colors"
|
||||
:class="nacosApiPlane === plane ? 'bg-background shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
||||
:aria-pressed="nacosApiPlane === plane"
|
||||
@click="nacosApiPlane = plane"
|
||||
>
|
||||
<span class="block text-sm font-medium">{{ t(`nacos.nacosApiPlane${plane === "admin" ? "Admin" : "Console"}`) }}</span>
|
||||
<span class="mt-0.5 block text-xs leading-4">{{ t(`nacos.nacosApiPlane${plane === "admin" ? "Admin" : "Console"}Hint`) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("nacos.nacosServiceAddress") }}</Label>
|
||||
<Input v-model="nacosServerAddr" :placeholder="nacosPrimaryAddressPlaceholder" />
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
<template>{{ t("nacos.nacosServiceAddressHint") }}</template>
|
||||
<template>{{ nacosServiceAddressHint }}</template>
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="nacosV3AdminEndpointWarning" class="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-xs leading-5 text-amber-700 dark:text-amber-400">
|
||||
@@ -6655,29 +6836,12 @@ function openExternalUrl(url: string) {
|
||||
<PasswordInput v-model="nacosPassword" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="nacosImplementation === 'nacos' && nacosAuthKind === 'usernamePassword'" data-nacos-ordinary-user-toggle class="mt-4 border-t pt-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-medium">{{ t("nacos.nacosOrdinaryAccount") }}</span>
|
||||
<span class="mt-0.5 block text-xs leading-5 text-muted-foreground">{{ t("nacos.nacosOrdinaryAccountHint") }}</span>
|
||||
</span>
|
||||
<Switch v-model="nacosOrdinaryAccount" class="mt-0.5 shrink-0" />
|
||||
</div>
|
||||
<div v-if="nacosOrdinaryAccount" class="mt-3 grid gap-1.5 pl-0 sm:pl-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Label>{{ t("nacos.nacosManagedNamespaces") }}</Label>
|
||||
<span class="text-[11px] text-muted-foreground">{{ t("nacos.nacosManagedNamespacesSeparator") }}</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="nacosManagedNamespacesText"
|
||||
data-nacos-managed-namespaces
|
||||
rows="2"
|
||||
class="min-h-14 resize-y rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm outline-none placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring"
|
||||
:placeholder="t('nacos.nacosManagedNamespacesPlaceholder')"
|
||||
/>
|
||||
<p class="text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosManagedNamespacesHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="isNacosV3AdminPlane && nacosAuthKind === 'usernamePassword'" class="mt-4 border-t pt-4 text-xs leading-5 text-muted-foreground">
|
||||
{{ t("nacos.nacosV3AdminNamespaceScopeHint") }}
|
||||
</p>
|
||||
<p v-else-if="nacosImplementation === 'rnacos' && nacosAuthKind === 'usernamePassword'" class="mt-4 border-t pt-4 text-xs leading-5 text-muted-foreground">
|
||||
{{ t("nacos.rnacosNamespaceAccessScopeHint") }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section data-nacos-advanced-hint class="flex items-start gap-3 rounded-lg border border-dashed bg-muted/20 px-4 py-3">
|
||||
@@ -8106,6 +8270,17 @@ function openExternalUrl(url: string) {
|
||||
<p class="mt-0.5 text-xs leading-5 text-muted-foreground">{{ t("nacos.nacosAdvancedDescription") }}</p>
|
||||
</div>
|
||||
<div class="grid gap-5 p-4">
|
||||
<div class="grid gap-1.5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Label>{{ t("connection.nacosContextPath") }}</Label>
|
||||
<Button v-if="nacosContextPath" type="button" variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="nacosContextPath = ''">
|
||||
{{ t("connection.nacosContextPathRestoreAuto") }}
|
||||
</Button>
|
||||
</div>
|
||||
<Input v-model="nacosContextPath" :placeholder="t('connection.nacosContextPathPlaceholder')" />
|
||||
<p class="text-[11px] leading-4 text-muted-foreground">{{ t("nacos.nacosContextPathHint") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-1.5">
|
||||
<Label>{{ t("connection.nacosPageSize") }}</Label>
|
||||
<Input v-model.number="nacosPageSize" type="number" min="1" max="500" />
|
||||
@@ -8721,7 +8896,7 @@ function openExternalUrl(url: string) {
|
||||
<Button v-if="canChooseVisibleNacosNamespaces" variant="outline" class="shrink-0" :disabled="isTesting || isSaving || isLoadingVisibleNacosNamespaces || !hasRequiredConnectionTarget" @click="openVisibleNacosNamespacesPicker">
|
||||
<Loader2 v-if="isLoadingVisibleNacosNamespaces" class="mr-1.5 h-4 w-4 animate-spin" />
|
||||
<ListFilter v-else class="mr-1.5 h-4 w-4" />
|
||||
{{ t("nacos.nacosVisibleNamespacesTitle") }}
|
||||
{{ t(nacosNamespacePickerTitleKey) }}
|
||||
</Button>
|
||||
<Button v-else-if="canChooseVisibleDatabases" variant="outline" class="shrink-0" :disabled="isTesting || isSaving || isLoadingVisibleDatabases || !hasRequiredConnectionTarget" @click="openVisibleDatabasesPicker">
|
||||
<Loader2 v-if="isLoadingVisibleDatabases" class="mr-1.5 h-4 w-4 animate-spin" />
|
||||
@@ -8817,51 +8992,89 @@ function openExternalUrl(url: string) {
|
||||
<Dialog v-model:open="showVisibleNacosNamespacesDialog">
|
||||
<DialogContent class="sm:max-w-[460px]" @interact-outside.prevent @escape-key-down.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.nacosVisibleNamespacesTitle") }}</DialogTitle>
|
||||
<p class="text-sm text-muted-foreground">{{ t("nacos.nacosVisibleNamespacesDescription", { name: form.name || selectedProfile().label }) }}</p>
|
||||
<DialogTitle>{{ t(nacosNamespacePickerTitleKey) }}</DialogTitle>
|
||||
<p class="text-sm text-muted-foreground">{{ t(nacosNamespacePickerDescriptionKey, { name: form.name || selectedProfile().label }) }}</p>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="flex items-center gap-2 rounded-md border bg-background px-2">
|
||||
<Search class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<Input v-model="visibleNacosNamespaceSearchText" :placeholder="t('nacos.nacosSearchNamespaces')" class="h-8 border-0 px-0 shadow-none focus-visible:ring-0" :disabled="isLoadingVisibleNacosNamespaces || !!visibleNacosNamespaceError" />
|
||||
</div>
|
||||
<Tabs v-model="visibleNacosNamespaceAccessMode" class="grid gap-3">
|
||||
<TabsList class="grid h-9 w-full" :class="canDetectNacosNamespaceAccess ? 'grid-cols-2' : 'grid-cols-1'">
|
||||
<TabsTrigger value="automatic">{{ t("nacos.nacosNamespaceAccessAutomatic") }}</TabsTrigger>
|
||||
<TabsTrigger v-if="canDetectNacosNamespaceAccess" value="manual">{{ t("nacos.nacosNamespaceAccessManual") }}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ t("nacos.nacosSelectedNamespaces", { selected: visibleNacosNamespaceSelectedCount, total: visibleNacosNamespaces.length }) }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="selectAllVisibleNacosNamespaces">{{ t("nacos.nacosSelectAll") }}</button>
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="clearVisibleNacosNamespaceSelection">{{ t("nacos.nacosClearSelection") }}</button>
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="showAllVisibleNacosNamespaces">{{ t("nacos.nacosShowAll") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!isLoadingVisibleNacosNamespaces && !visibleNacosNamespaceError && !visibleNacosNamespaceCanSave" class="text-xs text-destructive">{{ t("nacos.nacosNamespaceSelectionRequired") }}</p>
|
||||
<TabsContent value="automatic" class="m-0 grid gap-3">
|
||||
<div class="flex items-center gap-2 rounded-md border bg-background px-2">
|
||||
<Search class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<Input v-model="visibleNacosNamespaceSearchText" :placeholder="t('nacos.nacosSearchNamespaces')" class="h-8 border-0 px-0 shadow-none focus-visible:ring-0" :disabled="isLoadingVisibleNacosNamespaces || !!visibleNacosNamespaceError" />
|
||||
</div>
|
||||
|
||||
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
||||
<div v-if="isLoadingVisibleNacosNamespaces" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
<div v-else-if="visibleNacosNamespaceError" class="p-3 text-sm text-destructive">{{ t("nacos.nacosLoadNamespacesFailed", { message: visibleNacosNamespaceError }) }}</div>
|
||||
<div v-else-if="!filteredVisibleNacosNamespaces.length" class="p-3 text-sm text-muted-foreground">{{ t("grid.noSearchResults") }}</div>
|
||||
<template v-else>
|
||||
<button
|
||||
v-for="namespace in filteredVisibleNacosNamespaces"
|
||||
:key="nacosNamespaceValue(namespace) || '__public__'"
|
||||
type="button"
|
||||
class="flex min-h-9 w-full min-w-0 items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none"
|
||||
@click="toggleVisibleNacosNamespace(nacosNamespaceValue(namespace))"
|
||||
>
|
||||
<CheckSquare v-if="visibleNacosNamespaceSelection.has(nacosNamespaceValue(namespace))" class="h-4 w-4 shrink-0 text-primary" />
|
||||
<Square v-else class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ nacosNamespaceLabel(namespace) }}</span>
|
||||
<span v-if="namespace.namespace && namespace.namespace !== nacosNamespaceLabel(namespace)" class="shrink-0 truncate text-xs text-muted-foreground">{{ namespace.namespace }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ t("nacos.nacosSelectedNamespaces", { selected: visibleNacosNamespaceSelectedCount, total: visibleNacosNamespaces.length }) }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="selectAllVisibleNacosNamespaces">{{ t("nacos.nacosSelectAll") }}</button>
|
||||
<button class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="clearVisibleNacosNamespaceSelection">{{ t("nacos.nacosClearSelection") }}</button>
|
||||
<button v-if="visibleNacosNamespaceDynamicAllSupported" class="hover:text-foreground disabled:opacity-50" :disabled="isLoadingVisibleNacosNamespaces" @click="showAllVisibleNacosNamespaces">{{ t("nacos.nacosShowAll") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!isLoadingVisibleNacosNamespaces && !visibleNacosNamespaceError && !visibleNacosNamespaceCanSave" class="text-xs text-destructive">{{ t("nacos.nacosNamespaceSelectionRequired") }}</p>
|
||||
|
||||
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
|
||||
<div v-if="isLoadingVisibleNacosNamespaces" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
<div v-else-if="visibleNacosNamespaceListingPermissionDenied" class="grid gap-3 p-3 text-sm text-destructive">
|
||||
<p>{{ visibleNacosNamespaceError }}</p>
|
||||
<Button v-if="canDetectNacosNamespaceAccess" variant="outline" size="sm" class="justify-self-start" @click="visibleNacosNamespaceAccessMode = 'manual'">
|
||||
{{ t("nacos.nacosNamespaceAccessManual") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else-if="visibleNacosNamespaceError" class="p-3 text-sm text-destructive">{{ t("nacos.nacosLoadNamespacesFailed", { message: visibleNacosNamespaceError }) }}</div>
|
||||
<div v-else-if="!filteredVisibleNacosNamespaces.length" class="p-3 text-sm text-muted-foreground">{{ t("grid.noSearchResults") }}</div>
|
||||
<template v-else>
|
||||
<button
|
||||
v-for="namespace in filteredVisibleNacosNamespaces"
|
||||
:key="nacosNamespaceValue(namespace) || '__public__'"
|
||||
type="button"
|
||||
class="flex min-h-9 w-full min-w-0 items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none"
|
||||
@click="toggleVisibleNacosNamespace(nacosNamespaceValue(namespace))"
|
||||
>
|
||||
<CheckSquare v-if="visibleNacosNamespaceSelection.has(nacosNamespaceValue(namespace))" class="h-4 w-4 shrink-0 text-primary" />
|
||||
<Square v-else class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ nacosNamespaceLabel(namespace) }}</span>
|
||||
<span v-if="namespace.namespace && namespace.namespace !== nacosNamespaceLabel(namespace)" class="shrink-0 truncate text-xs text-muted-foreground">{{ namespace.namespace }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="canDetectNacosNamespaceAccess" value="manual" class="m-0 grid gap-3">
|
||||
<p class="text-xs leading-5 text-muted-foreground">{{ t("nacos.nacosNamespaceAccessScopeHint") }}</p>
|
||||
<div v-if="isNacosV3AdminPlane" class="flex gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2.5 text-xs leading-5 text-amber-800 dark:text-amber-300">
|
||||
<ShieldAlert class="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<p>{{ t("nacos.nacosV3AdminManagedNamespacesHint") }}</p>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Label>{{ t(nacosManualNamespaceLabelKey) }}</Label>
|
||||
<span class="text-[11px] text-muted-foreground">{{ t("nacos.nacosManagedNamespacesSeparator") }}</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="nacosManagedNamespacesText"
|
||||
data-nacos-managed-namespaces
|
||||
rows="6"
|
||||
class="min-h-32 resize-y rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm outline-none placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring"
|
||||
:placeholder="t(nacosManualNamespacePlaceholderKey)"
|
||||
/>
|
||||
<p class="text-[11px] leading-4 text-muted-foreground">{{ t(nacosManualNamespaceHintKey) }}</p>
|
||||
</div>
|
||||
<p v-if="!visibleNacosNamespaceCanSave" class="text-xs text-destructive">{{ t("nacos.nacosOrdinaryNamespacesRequired") }}</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showVisibleNacosNamespacesDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="isLoadingVisibleNacosNamespaces || !!visibleNacosNamespaceError || !visibleNacosNamespaceCanSave" @click="saveVisibleNacosNamespaceSelection">{{ t("nacos.save") }}</Button>
|
||||
<Button :disabled="isResolvingManualNacosNamespaces || !visibleNacosNamespaceCanSave || (visibleNacosNamespaceAccessMode === 'automatic' && (isLoadingVisibleNacosNamespaces || !!visibleNacosNamespaceError))" @click="saveVisibleNacosNamespaceSelection">{{ t("nacos.save") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { KeyRound, Loader2, Pencil, Plus, RefreshCw, Search, ShieldCheck, Trash2, UserRound } from "@lucide/vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { KeyRound, Loader2, Pencil, Plus, RefreshCw, Search, ShieldCheck, ShieldPlus, Trash2, UserRound } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -11,9 +11,10 @@ import { Switch } from "@/components/ui/switch";
|
||||
import NacosNamespaceMultiSelect from "@/components/nacos/NacosNamespaceMultiSelect.vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { executeWithProductionContextGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { subscribeNacosNamespacesChanged, type NacosNamespacesChangedDetail } from "@/lib/nacos/nacosNamespaceCache";
|
||||
import { normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import type { NacosConnectionInfo, NacosNamespaceInfo, NacosNamespacePrivilege, NacosOperationCapability, NacosRoleBinding, NacosUserInfo } from "@/types/nacos";
|
||||
import type { NacosAdminConfig, NacosConnectionInfo, NacosNamespaceInfo, NacosNamespacePrivilege, NacosOperationCapability, NacosRoleBinding, NacosUserInfo } from "@/types/nacos";
|
||||
|
||||
const props = defineProps<{
|
||||
connectionId: string;
|
||||
@@ -30,6 +31,7 @@ type UserDialogMode = "create" | "edit" | "reset";
|
||||
const users = ref<NacosUserInfo[]>([]);
|
||||
const usersLoading = ref(false);
|
||||
const usersError = ref("");
|
||||
const userOperationNotice = ref<{ kind: "success" | "error"; message: string } | null>(null);
|
||||
const userSearch = ref("");
|
||||
const userPageNo = ref(1);
|
||||
const userPageSize = ref(20);
|
||||
@@ -53,6 +55,7 @@ const userFormNamespaces = ref<NacosNamespaceInfo[]>([]);
|
||||
const userFormNamespacesLoading = ref(false);
|
||||
const userFormNamespacesError = ref("");
|
||||
const pendingUserDelete = ref<NacosUserInfo | null>(null);
|
||||
const userDeleteConfirmation = ref("");
|
||||
const deletingUser = ref(false);
|
||||
const roleDialogOpen = ref(false);
|
||||
const roleDialogError = ref("");
|
||||
@@ -67,12 +70,15 @@ const rnacosCaptchaImage = ref("");
|
||||
const rnacosCaptcha = ref("");
|
||||
const rnacosAuthError = ref("");
|
||||
let rnacosRetry: (() => Promise<void>) | null = null;
|
||||
let latestUserFormNamespacesRequestId = 0;
|
||||
let stopNacosNamespacesChangedListener: (() => void) | null = null;
|
||||
|
||||
const accessControl = computed(() => props.connectionInfo?.capabilities.accessControl);
|
||||
const mode = computed(() => accessControl.value?.mode ?? "unavailable");
|
||||
const isRNacos = computed(() => mode.value === "embeddedRoles");
|
||||
const userTotalPages = computed(() => Math.max(1, Math.ceil(userTotal.value / Math.max(1, userPageSize.value))));
|
||||
const roleTotalPages = computed(() => Math.max(1, Math.ceil(roleTotal.value / Math.max(1, rolePageSize.value))));
|
||||
const userDeleteConfirmed = computed(() => userDeleteConfirmation.value === pendingUserDelete.value?.username);
|
||||
|
||||
function createUserForm() {
|
||||
return {
|
||||
@@ -264,20 +270,35 @@ async function loadRoles(page = rolePageNo.value) {
|
||||
|
||||
async function loadUserFormNamespaces(force = false) {
|
||||
if (!isRNacos.value || userFormNamespacesLoading.value || (userFormNamespaces.value.length && !force)) return;
|
||||
const connectionId = props.connectionId;
|
||||
const requestId = ++latestUserFormNamespacesRequestId;
|
||||
userFormNamespacesLoading.value = true;
|
||||
userFormNamespacesError.value = "";
|
||||
try {
|
||||
userFormNamespaces.value = normalizeNacosNamespacesForDisplay(await api.nacosListNamespaces(props.connectionId));
|
||||
const namespaces = normalizeNacosNamespacesForDisplay(await api.nacosListNamespaces(connectionId));
|
||||
if (requestId !== latestUserFormNamespacesRequestId || connectionId !== props.connectionId) return;
|
||||
userFormNamespaces.value = namespaces;
|
||||
} catch (error) {
|
||||
if (requestId !== latestUserFormNamespacesRequestId || connectionId !== props.connectionId) return;
|
||||
userFormNamespacesError.value = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
userFormNamespacesLoading.value = false;
|
||||
if (requestId === latestUserFormNamespacesRequestId) userFormNamespacesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleNacosNamespacesChanged(detail: NacosNamespacesChangedDetail) {
|
||||
if (detail.connectionId !== props.connectionId) return;
|
||||
latestUserFormNamespacesRequestId += 1;
|
||||
userFormNamespaces.value = [];
|
||||
userFormNamespacesError.value = "";
|
||||
userFormNamespacesLoading.value = false;
|
||||
if (userDialogOpen.value && isRNacos.value) void loadUserFormNamespaces(true);
|
||||
}
|
||||
|
||||
function openCreateUser() {
|
||||
userDialogMode.value = "create";
|
||||
userDialogError.value = "";
|
||||
userOperationNotice.value = null;
|
||||
userForm.value = createUserForm();
|
||||
userDialogOpen.value = true;
|
||||
void loadUserFormNamespaces();
|
||||
@@ -286,6 +307,7 @@ function openCreateUser() {
|
||||
function openEditUser(user: NacosUserInfo) {
|
||||
userDialogMode.value = isRNacos.value ? "edit" : "reset";
|
||||
userDialogError.value = "";
|
||||
userOperationNotice.value = null;
|
||||
const privilege = user.namespacePrivilege;
|
||||
userForm.value = {
|
||||
username: user.username,
|
||||
@@ -305,8 +327,10 @@ function openEditUser(user: NacosUserInfo) {
|
||||
watch(
|
||||
() => props.connectionId,
|
||||
() => {
|
||||
latestUserFormNamespacesRequestId += 1;
|
||||
userFormNamespaces.value = [];
|
||||
userFormNamespacesError.value = "";
|
||||
userFormNamespacesLoading.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -320,6 +344,7 @@ function toggleRNacosRole(role: string, checked: boolean) {
|
||||
async function saveUser() {
|
||||
if (userSaving.value) return;
|
||||
const username = userForm.value.username.trim();
|
||||
const resettingPassword = userDialogMode.value === "reset";
|
||||
if (!username || (userDialogMode.value !== "edit" && !userForm.value.password)) {
|
||||
userDialogError.value = t("nacos.userCredentialsRequired");
|
||||
return;
|
||||
@@ -350,14 +375,61 @@ async function saveUser() {
|
||||
}
|
||||
userDialogOpen.value = false;
|
||||
await loadUsers(1);
|
||||
if (resettingPassword) {
|
||||
const handledConnectionCredential = await syncCurrentConnectionPassword(username, userForm.value.password);
|
||||
if (!handledConnectionCredential) {
|
||||
userOperationNotice.value = { kind: "success", message: t("nacos.passwordResetSucceeded", { username }) };
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
userDialogError.value = error instanceof Error ? error.message : String(error);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
userDialogError.value = message;
|
||||
if (resettingPassword) userOperationNotice.value = { kind: "error", message: t("nacos.passwordResetFailed", { message }) };
|
||||
} finally {
|
||||
userSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncCurrentConnectionPassword(username: string, password: string): Promise<boolean> {
|
||||
const config = connectionStore.getConfig(props.connectionId);
|
||||
if (!config || config.db_type !== "nacos") return false;
|
||||
const hasExternalConfig = !!config.external_config && typeof config.external_config === "object";
|
||||
const externalConfig = (config.external_config || {}) as NacosAdminConfig;
|
||||
const primaryAuth = externalConfig.auth;
|
||||
const primaryMatches = (primaryAuth?.kind === "usernamePassword" && primaryAuth.username === username) || (!hasExternalConfig && config.username === username);
|
||||
const consoleAuth = externalConfig.rnacosConsoleAuth;
|
||||
const consoleMatches = consoleAuth?.kind === "usernamePassword" && consoleAuth.username === username;
|
||||
if (!primaryMatches && !consoleMatches) return false;
|
||||
if (config.save_password === false) {
|
||||
try {
|
||||
await api.replaceNacosSessionCredential(props.connectionId, username, password);
|
||||
userOperationNotice.value = { kind: "success", message: `${t("nacos.passwordResetSucceeded", { username })} ${t("nacos.currentSessionPasswordUpdated")}` };
|
||||
} catch {
|
||||
userOperationNotice.value = { kind: "success", message: `${t("nacos.passwordResetSucceeded", { username })} ${t("nacos.currentPasswordNotSaved")}` };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const nextExternal: NacosAdminConfig | undefined = hasExternalConfig
|
||||
? {
|
||||
...externalConfig,
|
||||
auth: primaryMatches ? { kind: "usernamePassword", username, password } : primaryAuth,
|
||||
rnacosConsoleAuth: consoleMatches ? { kind: "usernamePassword", username, password } : consoleAuth,
|
||||
}
|
||||
: undefined;
|
||||
try {
|
||||
await connectionStore.updateConnection({
|
||||
...config,
|
||||
password: primaryMatches ? password : config.password,
|
||||
external_config: nextExternal,
|
||||
});
|
||||
userOperationNotice.value = { kind: "success", message: `${t("nacos.passwordResetSucceeded", { username })} ${t("nacos.currentPasswordSaved")}` };
|
||||
} catch {
|
||||
userOperationNotice.value = { kind: "success", message: `${t("nacos.passwordResetSucceeded", { username })} ${t("nacos.currentPasswordNotSaved")}` };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function deleteUser() {
|
||||
const user = pendingUserDelete.value;
|
||||
if (!user || deletingUser.value) return;
|
||||
@@ -367,6 +439,7 @@ async function deleteUser() {
|
||||
await withRNacosAuthentication(async () => {
|
||||
await api.nacosDeleteUser(props.connectionId, user.username);
|
||||
pendingUserDelete.value = null;
|
||||
userDeleteConfirmation.value = "";
|
||||
await loadUsers(Math.min(userPageNo.value, userTotalPages.value));
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -376,6 +449,16 @@ async function deleteUser() {
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteUser(user: NacosUserInfo) {
|
||||
pendingUserDelete.value = user;
|
||||
userDeleteConfirmation.value = "";
|
||||
}
|
||||
|
||||
function closeDeleteUser() {
|
||||
pendingUserDelete.value = null;
|
||||
userDeleteConfirmation.value = "";
|
||||
}
|
||||
|
||||
function openAssignRole() {
|
||||
roleDialogError.value = "";
|
||||
roleForm.value = { username: roleUsername.value.trim(), role: "" };
|
||||
@@ -439,9 +522,20 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
stopNacosNamespacesChangedListener = subscribeNacosNamespacesChanged(handleNacosNamespacesChanged);
|
||||
if (props.tab === "users") void loadUsers(1);
|
||||
else void loadRoles(1);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
latestUserFormNamespacesRequestId += 1;
|
||||
stopNacosNamespacesChangedListener?.();
|
||||
stopNacosNamespacesChangedListener = null;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
refresh: () => (props.tab === "users" ? loadUsers() : loadRoles()),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -464,6 +558,9 @@ onMounted(() => {
|
||||
</header>
|
||||
<div v-if="!listUsersCapability().supported" class="border-b bg-muted/30 px-3 py-2 text-xs text-muted-foreground">{{ capabilityReason(listUsersCapability()) }}</div>
|
||||
<div v-if="usersError" class="border-b px-3 py-2 text-xs text-destructive">{{ usersError }}</div>
|
||||
<div v-if="userOperationNotice" class="border-b px-3 py-2 text-xs" :class="userOperationNotice.kind === 'success' ? 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' : 'bg-destructive/10 text-destructive'" role="status" aria-live="polite">
|
||||
{{ userOperationNotice.message }}
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<table class="w-full min-w-[680px] text-sm">
|
||||
<thead class="sticky top-0 z-10 bg-muted/70 text-left text-xs text-muted-foreground">
|
||||
@@ -493,7 +590,7 @@ onMounted(() => {
|
||||
<div class="flex justify-end gap-1">
|
||||
<Button size="sm" variant="ghost" class="h-7 w-7 p-0" :disabled="!isWritable(updateUserCapability())" :title="isRNacos ? t('nacos.editUser') : t('nacos.resetUserPassword')" @click="openEditUser(user)"
|
||||
><Pencil v-if="isRNacos" class="h-3.5 w-3.5" /><KeyRound v-else class="h-3.5 w-3.5" /></Button
|
||||
><Button size="sm" variant="ghost" class="h-7 w-7 p-0 text-destructive hover:text-destructive" :disabled="!isWritable(deleteUserCapability())" :title="t('nacos.deleteUser')" @click="pendingUserDelete = user"><Trash2 class="h-3.5 w-3.5" /></Button>
|
||||
><Button size="sm" variant="ghost" class="h-7 w-7 p-0 text-destructive hover:text-destructive" :disabled="!isWritable(deleteUserCapability())" :title="t('nacos.deleteUser')" @click="openDeleteUser(user)"><Trash2 class="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -519,7 +616,7 @@ onMounted(() => {
|
||||
<Button size="sm" variant="outline" class="h-8 w-8 px-0" :disabled="rolesLoading || !listRolesCapability().supported" :title="!listRolesCapability().supported ? capabilityReason(listRolesCapability()) : t('nacos.refresh')" :aria-label="t('nacos.refresh')" @click="loadRoles(1)"
|
||||
><Loader2 v-if="rolesLoading" class="h-3.5 w-3.5 animate-spin" /><RefreshCw v-else class="h-3.5 w-3.5"
|
||||
/></Button>
|
||||
<Button size="sm" class="h-8 gap-1.5" :disabled="!isWritable(assignRoleCapability())" :title="!isWritable(assignRoleCapability()) ? capabilityReason(assignRoleCapability()) : undefined" @click="openAssignRole"><Plus class="h-3.5 w-3.5" />{{ t("nacos.assignRole") }}</Button>
|
||||
<Button size="sm" class="h-8 gap-1.5" :disabled="!isWritable(assignRoleCapability())" :title="!isWritable(assignRoleCapability()) ? capabilityReason(assignRoleCapability()) : undefined" @click="openAssignRole"><ShieldPlus class="h-3.5 w-3.5" />{{ t("nacos.assignRole") }}</Button>
|
||||
</header>
|
||||
<div v-if="rolesError" class="border-b px-3 py-2 text-xs text-destructive">{{ rolesError }}</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
@@ -645,14 +742,22 @@ onMounted(() => {
|
||||
></Dialog
|
||||
>
|
||||
|
||||
<Dialog :open="!!pendingUserDelete" @update:open="!$event && (pendingUserDelete = null)"
|
||||
<Dialog :open="!!pendingUserDelete" @update:open="!$event && closeDeleteUser()"
|
||||
><DialogContent class="sm:max-w-md"
|
||||
><DialogHeader
|
||||
><DialogTitle>{{ t("nacos.deleteUserTitle") }}</DialogTitle
|
||||
><DialogDescription>{{ t("nacos.deleteUserDescription", { username: pendingUserDelete?.username }) }}</DialogDescription></DialogHeader
|
||||
><DialogFooter
|
||||
><Button variant="outline" :disabled="deletingUser" @click="pendingUserDelete = null">{{ t("nacos.cancel") }}</Button
|
||||
><Button variant="destructive" :disabled="deletingUser" @click="deleteUser"><Loader2 v-if="deletingUser" class="mr-2 h-4 w-4 animate-spin" />{{ t("nacos.delete") }}</Button></DialogFooter
|
||||
>
|
||||
<div class="grid gap-1.5">
|
||||
<Label for="legacy-delete-user-confirm">{{ t("nacos.accessDeleteUserConfirmationLabel") }}</Label>
|
||||
<p id="legacy-delete-user-confirm-hint" class="text-xs text-muted-foreground">
|
||||
{{ t("nacos.accessDeleteUserConfirmationHint") }} <code class="select-all rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">{{ pendingUserDelete?.username }}</code>
|
||||
</p>
|
||||
<Input id="legacy-delete-user-confirm" v-model="userDeleteConfirmation" autocomplete="off" :placeholder="t('nacos.accessDeleteUserConfirmationPlaceholder')" aria-describedby="legacy-delete-user-confirm-hint" />
|
||||
</div>
|
||||
<DialogFooter
|
||||
><Button variant="outline" :disabled="deletingUser" @click="closeDeleteUser">{{ t("nacos.cancel") }}</Button
|
||||
><Button variant="destructive" :disabled="deletingUser || !userDeleteConfirmed" @click="deleteUser"><Loader2 v-if="deletingUser" class="mr-2 h-4 w-4 animate-spin" />{{ t("nacos.delete") }}</Button></DialogFooter
|
||||
></DialogContent
|
||||
></Dialog
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { KeyRound, Loader2, RefreshCw, ShieldCheck, UsersRound } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -16,6 +16,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
type AccessTab = "users" | "roles";
|
||||
type AccessControlWorkspace = { refresh: () => Promise<void> };
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
@@ -23,6 +24,8 @@ const connectionInfo = ref<NacosConnectionInfo | null>(null);
|
||||
const connectionError = ref("");
|
||||
const loading = ref(false);
|
||||
const activeTab = ref<AccessTab>("users");
|
||||
const legacyWorkspace = ref<AccessControlWorkspace | null>(null);
|
||||
const enhancedWorkspaceRef = ref<AccessControlWorkspace | null>(null);
|
||||
|
||||
const accessControl = computed(() => connectionInfo.value?.capabilities.accessControl);
|
||||
const supportsUsers = computed(() => accessControl.value?.listUsers.supported === true);
|
||||
@@ -30,13 +33,17 @@ const supportsRoles = computed(() => accessControl.value?.mode === "roleBindings
|
||||
const enhancedWorkspace = computed(() => accessControl.value?.enhancedWorkspace === true);
|
||||
const permissionWorkspaceUnavailable = computed(() => accessControl.value?.mode === "roleBindings" && accessControl.value.listPermissions?.supported === false && accessControl.value.listPermissions.reason !== "versionUnsupported");
|
||||
|
||||
async function loadConnectionInfo() {
|
||||
async function loadConnectionInfo(refreshWorkspace = false) {
|
||||
loading.value = true;
|
||||
connectionError.value = "";
|
||||
try {
|
||||
await connectionStore.ensureConnected(props.connectionId);
|
||||
connectionInfo.value = await api.nacosTestConnection(props.connectionId);
|
||||
connectionInfo.value = await api.nacosTestConnection(props.connectionId, refreshWorkspace);
|
||||
if (!supportsUsers.value && supportsRoles.value) activeTab.value = "roles";
|
||||
if (refreshWorkspace) {
|
||||
await nextTick();
|
||||
await (enhancedWorkspace.value ? enhancedWorkspaceRef.value : legacyWorkspace.value)?.refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
connectionInfo.value = null;
|
||||
connectionError.value = error instanceof Error ? error.message : String(error);
|
||||
@@ -74,7 +81,7 @@ watch(
|
||||
<Badge v-if="connectionInfo?.serverVersion" variant="secondary">{{ connectionInfo.serverVersion }}</Badge>
|
||||
<div class="flex-1" />
|
||||
<Badge v-if="readOnly" variant="outline">{{ t("nacos.readOnly") }}</Badge>
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="loading" @click="loadConnectionInfo">
|
||||
<Button size="sm" variant="outline" class="h-8 gap-1.5" :disabled="loading" @click="loadConnectionInfo(true)">
|
||||
<RefreshCw class="h-3.5 w-3.5" :class="loading ? 'animate-spin' : ''" />
|
||||
{{ t("nacos.refresh") }}
|
||||
</Button>
|
||||
@@ -88,8 +95,8 @@ watch(
|
||||
</div>
|
||||
<template v-else-if="connectionInfo">
|
||||
<div v-if="!supportsUsers && !supportsRoles" class="m-4 rounded border bg-muted/40 px-3 py-2 text-sm text-muted-foreground">{{ t("nacos.accessControlUnavailable") }}</div>
|
||||
<NacosRoleAccessControl v-if="enhancedWorkspace && accessControl" :connection-id="connectionId" :capabilities="accessControl" :read-only="readOnly" :tab="activeTab" @select-user="activeTab = 'users'" @select-role="activeTab = 'roles'" />
|
||||
<NacosAccessControl v-else-if="supportsUsers || supportsRoles" :connection-id="connectionId" :connection-info="connectionInfo" :read-only="readOnly" :tab="activeTab" />
|
||||
<NacosRoleAccessControl v-if="enhancedWorkspace && accessControl" ref="enhancedWorkspaceRef" :connection-id="connectionId" :capabilities="accessControl" :read-only="readOnly" :tab="activeTab" @select-user="activeTab = 'users'" @select-role="activeTab = 'roles'" />
|
||||
<NacosAccessControl v-else-if="supportsUsers || supportsRoles" ref="legacyWorkspace" :connection-id="connectionId" :connection-info="connectionInfo" :read-only="readOnly" :tab="activeTab" />
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
@@ -313,6 +313,7 @@ const updateServiceCapability = computed(() => operationCapability(serviceCapabi
|
||||
const deleteServiceCapability = computed(() => operationCapability(serviceCapabilities.value?.deleteService, createServiceCapability.value.supported));
|
||||
const listInstancesCapability = computed(() => operationCapability(serviceCapabilities.value?.listInstances, listServicesCapability.value.supported));
|
||||
const updateInstanceCapability = computed(() => operationCapability(serviceCapabilities.value?.updateInstance, legacyInstanceUpdateSupported.value));
|
||||
const updateInstanceHealthCapability = computed(() => operationCapability(serviceCapabilities.value?.updateInstanceHealth, updateInstanceCapability.value.supported));
|
||||
const registerInstanceCapability = computed(() => operationCapability(serviceCapabilities.value?.registerInstance, updateInstanceCapability.value.supported));
|
||||
const deregisterInstanceCapability = computed(() => operationCapability(serviceCapabilities.value?.deregisterInstance, updateInstanceCapability.value.supported));
|
||||
const supportsServiceManagement = computed(() => listServicesCapability.value.supported);
|
||||
@@ -2876,9 +2877,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<div class="flex items-center gap-1 rounded-md border bg-background p-1">
|
||||
<span class="px-1 text-xs text-muted-foreground">{{ t("nacos.instances") }}</span>
|
||||
<Button size="sm" class="h-7" :disabled="readOnly || !registerInstanceCapability.supported" :title="readOnly || !registerInstanceCapability.supported ? capabilityReason(registerInstanceCapability) : undefined" @click="registerInstanceOpen = true">{{
|
||||
t("nacos.registerInstance")
|
||||
}}</Button>
|
||||
<Button v-if="registerInstanceCapability.supported" size="sm" class="h-7" :disabled="readOnly" :title="readOnly ? capabilityReason(registerInstanceCapability) : undefined" @click="registerInstanceOpen = true">{{ t("nacos.registerInstance") }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2986,15 +2985,16 @@ onBeforeUnmount(() => {
|
||||
<Loader2 v-if="isInstanceUpdating(instance)" class="h-3 w-3 animate-spin" />
|
||||
{{ instance.enabled === false ? t("nacos.enable") : t("nacos.disable") }}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="h-7" :disabled="readOnly || !supportsInstanceUpdate || isInstanceUpdating(instance)" @click="requestUpdateInstance(instance, { healthy: !instance.healthy })">
|
||||
<Button v-if="updateInstanceHealthCapability.supported" size="sm" variant="outline" class="h-7" :disabled="readOnly || isInstanceUpdating(instance)" @click="requestUpdateInstance(instance, { healthy: !instance.healthy })">
|
||||
{{ instance.healthy === false ? t("nacos.markHealthy") : t("nacos.markUnhealthy") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="deregisterInstanceCapability.supported"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 text-destructive"
|
||||
:disabled="readOnly || !deregisterInstanceCapability.supported || isInstanceUpdating(instance)"
|
||||
:title="readOnly || !deregisterInstanceCapability.supported ? capabilityReason(deregisterInstanceCapability) : undefined"
|
||||
:disabled="readOnly || isInstanceUpdating(instance)"
|
||||
:title="readOnly ? capabilityReason(deregisterInstanceCapability) : undefined"
|
||||
@click="pendingInstanceDeregister = instance"
|
||||
>{{ t("nacos.deregister") }}</Button
|
||||
>
|
||||
|
||||
@@ -368,11 +368,11 @@ onUnmounted(() => {
|
||||
<MetricCard :label="t('nacos.dashboardNamespaces')" :value="optionalCount(snapshot?.namespaceCount)" :icon="Layers3" />
|
||||
<MetricCard :label="t('nacos.dashboardConfigs')" :value="optionalCount(snapshot?.configCount)" :sub="namespaceLabel" :icon="Braces" />
|
||||
<MetricCard :label="t('nacos.dashboardServices')" :value="optionalCount(snapshot?.serviceCount)" :sub="namespaceLabel" :icon="Boxes" />
|
||||
<MetricCard :label="t('nacos.dashboardInstances')" :value="optionalCount(metrics?.instanceCount)" :icon="Network" />
|
||||
<MetricCard :label="t('nacos.dashboardClients')" :value="optionalCount(metrics?.clientCount)" :sub="t('nacos.dashboardConnections', { count: optionalCount(metrics?.connectionBasedClientCount) })" :icon="Users" />
|
||||
<MetricCard v-if="metrics" :label="t('nacos.dashboardInstances')" :value="optionalCount(metrics.instanceCount)" :icon="Network" />
|
||||
<MetricCard v-if="metrics" :label="t('nacos.dashboardClients')" :value="optionalCount(metrics.clientCount)" :sub="t('nacos.dashboardConnections', { count: optionalCount(metrics.connectionBasedClientCount) })" :icon="Users" />
|
||||
<MetricCard :label="t('nacos.dashboardNodes')" :value="snapshot?.nodes.length ? `${healthyNodes} / ${snapshot.nodes.length}` : '—'" :icon="Server" />
|
||||
<MetricCard :label="t('nacos.dashboardCpu')" :value="formatDashboardPercent(metrics?.cpu)" :icon="Cpu" />
|
||||
<MetricCard :label="t('nacos.dashboardMemory')" :value="formatDashboardPercent(metrics?.mem)" :sub="t('nacos.dashboardLoad', { value: optionalMetric(metrics?.load) })" :icon="HardDrive" />
|
||||
<MetricCard v-if="metrics" :label="t('nacos.dashboardCpu')" :value="formatDashboardPercent(metrics.cpu)" :icon="Cpu" />
|
||||
<MetricCard v-if="metrics" :label="t('nacos.dashboardMemory')" :value="formatDashboardPercent(metrics.mem)" :sub="t('nacos.dashboardLoad', { value: optionalMetric(metrics.load) })" :icon="HardDrive" />
|
||||
</div>
|
||||
|
||||
<div v-if="prometheus" class="grid shrink-0 grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { AlertTriangle, Check, ChevronLeft, ChevronRight, CircleAlert, KeyRound, Link2, Loader2, LockKeyhole, Pencil, Plus, RefreshCw, RotateCcw, Search, Shield, ShieldCheck, Trash2, UserRound, X } from "@lucide/vue";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { AlertTriangle, Check, ChevronLeft, ChevronRight, CircleAlert, KeyRound, Link2, Loader2, LockKeyhole, Pencil, Plus, RefreshCw, RotateCcw, Search, Shield, ShieldCheck, ShieldPlus, Trash2, UserPlus, UserRound, X } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { executeWithProductionContextGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { mergeNacosNamespacePermissionAssignments, type NacosNamespacePermissionAction } from "@/lib/nacos/nacosAdmin";
|
||||
import { subscribeNacosNamespacesChanged, type NacosNamespacesChangedDetail } from "@/lib/nacos/nacosNamespaceCache";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import type { NacosAccessControlCapabilities, NacosAccessControlSnapshot, NacosAccessOperationRequest, NacosAccessOperationResult, NacosPermissionDraft, NacosPermissionInfo, NacosUserInfo } from "@/types/nacos";
|
||||
import type { NacosAccessControlCapabilities, NacosAccessControlSnapshot, NacosAccessOperationRequest, NacosAccessOperationResult, NacosAdminConfig, NacosPermissionDraft, NacosPermissionInfo, NacosUserInfo } from "@/types/nacos";
|
||||
|
||||
const props = defineProps<{
|
||||
connectionId: string;
|
||||
@@ -28,6 +30,7 @@ type PermissionAction = NacosNamespacePermissionAction;
|
||||
type PermissionAssignment = { namespaceId: string; action: PermissionAction };
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const connectionStore = useConnectionStore();
|
||||
const snapshot = ref<NacosAccessControlSnapshot | null>(null);
|
||||
const loading = ref(false);
|
||||
@@ -59,6 +62,8 @@ const operation = ref<NacosAccessOperationResult | null>(null);
|
||||
const pendingAdvancedPermission = ref<NacosPermissionInfo | null>(null);
|
||||
const advancedPermissionConfirmation = ref("");
|
||||
const retryPasswords = reactive<Record<string, string>>({});
|
||||
let latestSnapshotRequestId = 0;
|
||||
let stopNacosNamespacesChangedListener: (() => void) | null = null;
|
||||
|
||||
const users = computed(() => snapshot.value?.users ?? []);
|
||||
const roles = computed(() => snapshot.value?.roles ?? []);
|
||||
@@ -122,6 +127,8 @@ const canRevokePermission = computed(() => !props.readOnly && props.capabilities
|
||||
const canCreateRole = computed(() => canAssignRole.value && canGrantPermission.value);
|
||||
const canEditUserRoles = computed(() => canAssignRole.value || canRemoveRole.value);
|
||||
const canEditRole = computed(() => canAssignRole.value || canRemoveRole.value || canGrantPermission.value || canRevokePermission.value || canCreateUser.value);
|
||||
const userDeleteConfirmed = computed(() => userForm.confirmation === userForm.username);
|
||||
const roleDeleteConfirmed = computed(() => roleForm.confirmation === roleForm.role);
|
||||
|
||||
function setHasAdded(before: Iterable<string>, after: Iterable<string>) {
|
||||
const original = new Set(before);
|
||||
@@ -179,10 +186,14 @@ const roleOperationWritable = computed(() => {
|
||||
});
|
||||
|
||||
async function loadSnapshot(preserveSelection = true) {
|
||||
const requestId = ++latestSnapshotRequestId;
|
||||
const connectionId = props.connectionId;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
snapshot.value = await api.nacosAccessSnapshot(props.connectionId);
|
||||
const nextSnapshot = await api.nacosAccessSnapshot(connectionId);
|
||||
if (requestId !== latestSnapshotRequestId || connectionId !== props.connectionId) return;
|
||||
snapshot.value = nextSnapshot;
|
||||
if (!preserveSelection || !users.value.some((user) => user.username === selectedUserName.value)) {
|
||||
selectedUserName.value = users.value[0]?.username ?? "";
|
||||
}
|
||||
@@ -190,12 +201,17 @@ async function loadSnapshot(preserveSelection = true) {
|
||||
selectedRoleName.value = roles.value[0]?.role ?? "";
|
||||
}
|
||||
} catch (cause) {
|
||||
if (requestId !== latestSnapshotRequestId || connectionId !== props.connectionId) return;
|
||||
error.value = cause instanceof Error ? cause.message : String(cause);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (requestId === latestSnapshotRequestId) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleNacosNamespacesChanged(detail: NacosNamespacesChangedDetail) {
|
||||
if (detail.connectionId === props.connectionId) void loadSnapshot();
|
||||
}
|
||||
|
||||
function isManagedPermission(permission: NacosPermissionInfo) {
|
||||
return permission.parsedScope?.kind === "namespace" && ["r", "w", "rw"].includes(permission.actionRaw);
|
||||
}
|
||||
@@ -317,6 +333,45 @@ function openDeleteUser() {
|
||||
userDialog.value = "delete";
|
||||
}
|
||||
|
||||
async function syncCurrentConnectionPassword(username: string, password: string) {
|
||||
if (snapshot.value?.currentUsername !== username) return;
|
||||
const config = connectionStore.getConfig(props.connectionId);
|
||||
if (!config || config.db_type !== "nacos") return;
|
||||
const hasExternalConfig = !!config.external_config && typeof config.external_config === "object";
|
||||
const externalConfig = (config.external_config || {}) as NacosAdminConfig;
|
||||
const primaryAuth = externalConfig.auth;
|
||||
const primaryMatches = (primaryAuth?.kind === "usernamePassword" && primaryAuth.username === username) || (!hasExternalConfig && config.username === username);
|
||||
const consoleAuth = externalConfig.rnacosConsoleAuth;
|
||||
const consoleMatches = consoleAuth?.kind === "usernamePassword" && consoleAuth.username === username;
|
||||
if (!primaryMatches && !consoleMatches) return;
|
||||
if (config.save_password === false) {
|
||||
try {
|
||||
await api.replaceNacosSessionCredential(props.connectionId, username, password);
|
||||
toast(t("nacos.currentSessionPasswordUpdated"), 4000);
|
||||
} catch {
|
||||
toast(t("nacos.currentPasswordNotSaved"), 5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const nextExternal: NacosAdminConfig | undefined = hasExternalConfig
|
||||
? {
|
||||
...externalConfig,
|
||||
auth: primaryMatches ? { kind: "usernamePassword", username, password } : primaryAuth,
|
||||
rnacosConsoleAuth: consoleMatches ? { kind: "usernamePassword", username, password } : consoleAuth,
|
||||
}
|
||||
: undefined;
|
||||
try {
|
||||
await connectionStore.updateConnection({
|
||||
...config,
|
||||
password: primaryMatches ? password : config.password,
|
||||
external_config: nextExternal,
|
||||
});
|
||||
toast(t("nacos.currentPasswordSaved"), 4000);
|
||||
} catch {
|
||||
toast(t("nacos.currentPasswordNotSaved"), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function resetRoleForm(role = "") {
|
||||
Object.assign(roleForm, { role, members: [], newUsers: [], permissions: [], confirmation: "" });
|
||||
availableNamespaceSearch.value = "";
|
||||
@@ -342,7 +397,7 @@ function openEditRole() {
|
||||
|
||||
function openDeleteRole() {
|
||||
if (!selectedRole.value) return;
|
||||
roleForm.confirmation = "";
|
||||
resetRoleForm(selectedRole.value.role);
|
||||
formError.value = "";
|
||||
roleDialog.value = "delete";
|
||||
}
|
||||
@@ -360,7 +415,10 @@ function openAssociatedRole(role: string) {
|
||||
function filterNamespaces<T extends { id?: string; namespaceId?: string; name: string }>(items: T[], query: string) {
|
||||
const normalized = query.trim().toLocaleLowerCase();
|
||||
if (!normalized) return items;
|
||||
return items.filter((item) => `${item.name} ${item.id ?? item.namespaceId ?? ""}`.toLocaleLowerCase().includes(normalized));
|
||||
return items.filter((item) => {
|
||||
const id = item.id ?? item.namespaceId ?? "";
|
||||
return item.name.toLocaleLowerCase().includes(normalized) || id.toLocaleLowerCase().includes(normalized);
|
||||
});
|
||||
}
|
||||
|
||||
function moveNamespacesToGranted() {
|
||||
@@ -393,12 +451,17 @@ async function submitUser() {
|
||||
if (userDialog.value === "password") {
|
||||
if (!userForm.password) return void (formError.value = t("nacos.accessPasswordRequired"));
|
||||
if (!(await confirmAccessMutation(`${t("nacos.accessUserDialog.password")}: ${userForm.username}`))) return;
|
||||
const username = userForm.username;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.nacosUpdateUser(props.connectionId, { username: userForm.username, password: userForm.password });
|
||||
await api.nacosUpdateUser(props.connectionId, { username, password: userForm.password });
|
||||
await syncCurrentConnectionPassword(username, userForm.password);
|
||||
userDialog.value = null;
|
||||
toast(t("nacos.passwordResetSucceeded", { username }), 2500);
|
||||
} catch (cause) {
|
||||
formError.value = cause instanceof Error ? cause.message : String(cause);
|
||||
const message = cause instanceof Error ? cause.message : String(cause);
|
||||
formError.value = message;
|
||||
toast(t("nacos.passwordResetFailed", { message }), 5000);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -422,7 +485,8 @@ async function submitRole() {
|
||||
await runOperation({ kind: "deleteRole", role: roleForm.role, confirmation: roleForm.confirmation || undefined }, () => (roleDialog.value = null));
|
||||
return;
|
||||
}
|
||||
if (!roleForm.role.trim() || (!roleForm.members.length && !roleForm.newUsers.length)) {
|
||||
const roleRequiresMember = roleDialog.value === "create" || selectedRole.value?.administrator;
|
||||
if (!roleForm.role.trim() || (roleRequiresMember && !roleForm.members.length && !roleForm.newUsers.length)) {
|
||||
formError.value = t("nacos.accessRoleMemberRequired");
|
||||
return;
|
||||
}
|
||||
@@ -518,7 +582,17 @@ watch(
|
||||
},
|
||||
);
|
||||
watch(normalizedSearch, () => (page.value = 1));
|
||||
onMounted(() => void loadSnapshot(false));
|
||||
onMounted(() => {
|
||||
stopNacosNamespacesChangedListener = subscribeNacosNamespacesChanged(handleNacosNamespacesChanged);
|
||||
void loadSnapshot(false);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
latestSnapshotRequestId += 1;
|
||||
stopNacosNamespacesChangedListener?.();
|
||||
stopNacosNamespacesChangedListener = null;
|
||||
});
|
||||
|
||||
defineExpose({ refresh: () => loadSnapshot() });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -529,8 +603,16 @@ onMounted(() => void loadSnapshot(false));
|
||||
<Search class="pointer-events-none absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input v-model="search" class="h-8 pl-8" :placeholder="tab === 'users' ? t('nacos.searchUsers') : t('nacos.accessSearchRoles')" />
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" class="h-8 w-8 p-0" :aria-label="t('nacos.refresh')" :disabled="loading" @click="loadSnapshot()">
|
||||
<RefreshCw class="h-3.5 w-3.5" :class="loading ? 'animate-spin' : ''" />
|
||||
<Button
|
||||
size="sm"
|
||||
class="h-8 w-8 shrink-0 p-0"
|
||||
:title="tab === 'users' ? t('nacos.createUser') : t('nacos.accessCreateRole')"
|
||||
:aria-label="tab === 'users' ? t('nacos.createUser') : t('nacos.accessCreateRole')"
|
||||
:disabled="tab === 'users' ? !canCreateUser : !canCreateRole"
|
||||
@click="tab === 'users' ? openCreateUser() : openCreateRole()"
|
||||
>
|
||||
<UserPlus v-if="tab === 'users'" class="h-3.5 w-3.5" />
|
||||
<ShieldPlus v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -554,8 +636,7 @@ onMounted(() => void loadSnapshot(false));
|
||||
<div v-if="!loading && !currentItems.length" class="flex h-32 items-center justify-center px-4 text-center text-xs text-muted-foreground">{{ tab === "users" ? t("nacos.noUsers") : t("nacos.accessNoRoles") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 space-y-2 border-t p-2">
|
||||
<Button class="h-8 w-full" :disabled="tab === 'users' ? !canCreateUser : !canCreateRole" @click="tab === 'users' ? openCreateUser() : openCreateRole()"><Plus class="mr-1.5 h-3.5 w-3.5" />{{ tab === "users" ? t("nacos.createUser") : t("nacos.accessCreateRole") }}</Button>
|
||||
<div class="shrink-0 border-t p-2">
|
||||
<div class="flex h-7 items-center justify-between px-1 text-xs text-muted-foreground">
|
||||
<span>{{ currentItems.length }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -787,20 +868,29 @@ onMounted(() => void loadSnapshot(false));
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="userDialog === 'delete'" class="rounded border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">{{ t("nacos.deleteUserDescription", { username: userForm.username }) }}</div>
|
||||
<div v-if="userForm.roles.includes('ROLE_ADMIN') || (snapshot?.currentUsername === userForm.username && userDialog !== 'password')" class="grid gap-1.5">
|
||||
<div v-if="userDialog === 'delete'" class="grid gap-1.5">
|
||||
<Label for="access-delete-user-confirm">{{ t("nacos.accessDeleteUserConfirmationLabel") }}</Label>
|
||||
<p id="access-delete-user-confirm-hint" class="text-xs text-muted-foreground">
|
||||
{{ t("nacos.accessDeleteUserConfirmationHint") }} <code class="select-all rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">{{ userForm.username }}</code>
|
||||
</p>
|
||||
<Input id="access-delete-user-confirm" v-model="userForm.confirmation" autocomplete="off" :placeholder="t('nacos.accessDeleteUserConfirmationPlaceholder')" aria-describedby="access-delete-user-confirm-hint" />
|
||||
</div>
|
||||
<div v-if="userDialog !== 'delete' && (userForm.roles.includes('ROLE_ADMIN') || (snapshot?.currentUsername === userForm.username && userDialog !== 'password'))" class="grid gap-1.5">
|
||||
<Label for="access-user-confirm">{{ t("nacos.accessTypeUsername", { value: userForm.username }) }}</Label
|
||||
><Input id="access-user-confirm" v-model="userForm.confirmation" autocomplete="off" />
|
||||
</div>
|
||||
<p v-if="formError" class="text-sm text-destructive">{{ formError }}</p>
|
||||
<DialogFooter
|
||||
><Button variant="outline" :disabled="saving" @click="userDialog = null">{{ t("nacos.cancel") }}</Button
|
||||
><Button :variant="userDialog === 'delete' ? 'destructive' : 'default'" :disabled="saving || !userOperationWritable" @click="submitUser"><Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />{{ userDialog === "delete" ? t("nacos.delete") : t("nacos.save") }}</Button></DialogFooter
|
||||
><Button :variant="userDialog === 'delete' ? 'destructive' : 'default'" :disabled="saving || !userOperationWritable || (userDialog === 'delete' && !userDeleteConfirmed)" @click="submitUser"
|
||||
><Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />{{ userDialog === "delete" ? t("nacos.delete") : t("nacos.save") }}</Button
|
||||
></DialogFooter
|
||||
>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog :open="!!roleDialog" @update:open="!$event && (roleDialog = null)">
|
||||
<DialogContent class="max-h-[90vh] overflow-auto sm:max-w-2xl">
|
||||
<DialogContent class="max-h-[90vh] overflow-auto sm:max-w-3xl">
|
||||
<DialogHeader
|
||||
><DialogTitle>{{ t(`nacos.accessRoleDialog.${roleDialog}`) }}</DialogTitle
|
||||
><DialogDescription>{{ t(`nacos.accessRoleDialog.${roleDialog}Hint`) }}</DialogDescription></DialogHeader
|
||||
@@ -815,7 +905,9 @@ onMounted(() => void loadSnapshot(false));
|
||||
<div class="mb-2 flex items-end justify-between">
|
||||
<div>
|
||||
<Label>{{ t("nacos.accessMembers") }}</Label>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">{{ t("nacos.accessMembersRequiredHint") }}</p>
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">
|
||||
{{ t(roleDialog === "edit" && !selectedRole?.administrator ? "nacos.accessMembersOptionalHint" : "nacos.accessMembersRequiredHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" :disabled="!canCreateUser || !canAssignRole" @click="addNewUser"><Plus class="mr-1 h-3.5 w-3.5" />{{ t("nacos.accessInlineCreateUser") }}</Button>
|
||||
</div>
|
||||
@@ -843,17 +935,21 @@ onMounted(() => void loadSnapshot(false));
|
||||
<p class="mt-0.5 text-xs text-muted-foreground">{{ t("nacos.accessPermissionRequiredHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid min-h-[17rem] grid-cols-[minmax(0,1fr)_2.5rem_minmax(0,1.15fr)] gap-2 sm:gap-3">
|
||||
<div class="grid h-[min(28rem,45vh)] min-h-[17rem] grid-cols-[minmax(0,1fr)_2.5rem_minmax(0,1.15fr)] gap-2 sm:gap-3">
|
||||
<div class="flex min-h-0 flex-col overflow-hidden rounded border">
|
||||
<div class="border-b bg-muted/30 px-3 py-2 text-sm font-medium">{{ t("nacos.accessUnassignedNamespaces") }}</div>
|
||||
<div class="border-b p-2">
|
||||
<div class="relative"><Search class="pointer-events-none absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" /><Input v-model="availableNamespaceSearch" class="h-8 pl-8 text-sm" :placeholder="t('nacos.accessSearchNamespaces')" /></div>
|
||||
<div class="relative">
|
||||
<Search class="pointer-events-none absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" /><Input v-model="availableNamespaceSearch" class="h-8 pl-8 text-sm" :placeholder="t('nacos.accessSearchNamespaces')" :aria-label="t('nacos.accessSearchNamespaces')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto p-1.5">
|
||||
<label v-for="namespace in availableNamespaces" :key="namespace.id" class="flex min-h-9 items-center gap-2 rounded px-2 text-sm hover:bg-muted/60">
|
||||
<label v-for="namespace in availableNamespaces" :key="namespace.id" class="flex min-h-11 items-center gap-2 rounded px-2 py-1 text-sm hover:bg-muted/60">
|
||||
<input type="checkbox" :checked="selectedAvailableNamespaceIds.includes(namespace.id)" @change="selectedAvailableNamespaceIds = toggle(selectedAvailableNamespaceIds, namespace.id, ($event.target as HTMLInputElement).checked)" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ namespace.name }}</span>
|
||||
<code v-if="namespace.name !== namespace.id" class="truncate text-[10px] text-muted-foreground">{{ namespace.id }}</code>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-medium" :title="namespace.name">{{ namespace.name }}</span>
|
||||
<code v-if="namespace.name !== namespace.id" class="block break-all text-[10px] leading-4 text-muted-foreground" :title="namespace.id">{{ namespace.id }}</code>
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="!availableNamespaces.length" class="flex h-24 items-center justify-center px-3 text-center text-xs text-muted-foreground">{{ t("nacos.accessNoUnassignedNamespaces") }}</div>
|
||||
</div>
|
||||
@@ -865,12 +961,16 @@ onMounted(() => void loadSnapshot(false));
|
||||
<div class="flex min-h-0 flex-col overflow-hidden rounded border">
|
||||
<div class="border-b bg-muted/30 px-3 py-2 text-sm font-medium">{{ t("nacos.accessGrantedNamespaces") }}</div>
|
||||
<div class="border-b p-2">
|
||||
<div class="relative"><Search class="pointer-events-none absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" /><Input v-model="grantedNamespaceSearch" class="h-8 pl-8 text-sm" :placeholder="t('nacos.accessSearchNamespaces')" /></div>
|
||||
<div class="relative">
|
||||
<Search class="pointer-events-none absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" /><Input v-model="grantedNamespaceSearch" class="h-8 pl-8 text-sm" :placeholder="t('nacos.accessSearchNamespaces')" :aria-label="t('nacos.accessSearchNamespaces')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto p-1.5">
|
||||
<div v-for="namespace in grantedNamespaces" :key="namespace.namespaceId" class="flex min-h-10 items-center gap-2 rounded px-2 text-sm hover:bg-muted/60">
|
||||
<div v-for="namespace in grantedNamespaces" :key="namespace.namespaceId" class="flex min-h-11 items-center gap-2 rounded px-2 py-1 text-sm hover:bg-muted/60">
|
||||
<input type="checkbox" :checked="selectedGrantedNamespaceIds.includes(namespace.namespaceId)" @change="selectedGrantedNamespaceIds = toggle(selectedGrantedNamespaceIds, namespace.namespaceId, ($event.target as HTMLInputElement).checked)" />
|
||||
<span class="min-w-0 flex-1 truncate font-medium">{{ namespace.name }}</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate font-medium" :title="namespace.name">{{ namespace.name }}</span>
|
||||
</span>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<label
|
||||
v-for="action in ['r', 'w', 'rw'] as PermissionAction[]"
|
||||
@@ -903,13 +1003,18 @@ onMounted(() => void loadSnapshot(false));
|
||||
<template v-else
|
||||
><div class="rounded border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">{{ t("nacos.accessDeleteRoleHint", { role: roleForm.role }) }}</div>
|
||||
<div class="grid gap-1.5">
|
||||
<Label for="access-delete-role-confirm">{{ t("nacos.accessTypeRole", { value: roleForm.role }) }}</Label
|
||||
><Input id="access-delete-role-confirm" v-model="roleForm.confirmation" /></div
|
||||
<Label for="access-delete-role-confirm">{{ t("nacos.accessDeleteRoleConfirmationLabel") }}</Label>
|
||||
<p id="access-delete-role-confirm-hint" class="text-xs text-muted-foreground">
|
||||
{{ t("nacos.accessDeleteRoleConfirmationHint") }} <code class="select-all rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">{{ roleForm.role }}</code>
|
||||
</p>
|
||||
<Input id="access-delete-role-confirm" v-model="roleForm.confirmation" autocomplete="off" :placeholder="t('nacos.accessDeleteRoleConfirmationPlaceholder')" aria-describedby="access-delete-role-confirm-hint" /></div
|
||||
></template>
|
||||
<p v-if="formError" class="text-sm text-destructive">{{ formError }}</p>
|
||||
<DialogFooter
|
||||
><Button variant="outline" :disabled="saving" @click="roleDialog = null">{{ t("nacos.cancel") }}</Button
|
||||
><Button :variant="roleDialog === 'delete' ? 'destructive' : 'default'" :disabled="saving || !roleOperationWritable" @click="submitRole"><Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />{{ roleDialog === "delete" ? t("nacos.delete") : t("nacos.save") }}</Button></DialogFooter
|
||||
><Button :variant="roleDialog === 'delete' ? 'destructive' : 'default'" :disabled="saving || !roleOperationWritable || (roleDialog === 'delete' && !roleDeleteConfirmed)" @click="submitRole"
|
||||
><Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />{{ roleDialog === "delete" ? t("nacos.delete") : t("nacos.save") }}</Button
|
||||
></DialogFooter
|
||||
>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
+98
-9
@@ -9,15 +9,18 @@ import type { NacosConnectionInfo } from "@/types/nacos";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
connectionInfo: null as NacosConnectionInfo | null,
|
||||
ensureConnected: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
refreshLegacyWorkspace: vi.fn(async () => undefined),
|
||||
refreshEnhancedWorkspace: vi.fn(async () => undefined),
|
||||
nacosTestConnection: vi.fn(async () => {
|
||||
if (!mocks.connectionInfo) throw new Error("missing Nacos connection fixture");
|
||||
return mocks.connectionInfo;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
nacosTestConnection: (...args: unknown[]) => mocks.nacosTestConnection(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => ({ ensureConnected: mocks.ensureConnected }),
|
||||
}));
|
||||
@@ -31,7 +34,10 @@ vi.mock("@/components/nacos/NacosAccessControl.vue", async () => {
|
||||
return {
|
||||
default: {
|
||||
props: ["tab"],
|
||||
setup: (props: { tab: string }) => () => h("div", { "data-testid": "nacos-access-control-workspace", "data-tab": props.tab }),
|
||||
setup: (props: { tab: string }, { expose }: { expose: (value: { refresh: () => Promise<void> }) => void }) => {
|
||||
expose({ refresh: mocks.refreshLegacyWorkspace });
|
||||
return () => h("div", { "data-testid": "nacos-access-control-workspace", "data-tab": props.tab });
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -41,11 +47,14 @@ vi.mock("@/components/nacos/NacosRoleAccessControl.vue", async () => {
|
||||
return {
|
||||
default: {
|
||||
props: ["capabilities"],
|
||||
setup: (props: { capabilities: { createUser: { supported: boolean } } }) => () =>
|
||||
h("div", {
|
||||
"data-testid": "nacos-role-access-control-workspace",
|
||||
"data-can-create-user": String(props.capabilities.createUser.supported),
|
||||
}),
|
||||
setup: (props: { capabilities: { createUser: { supported: boolean } } }, { expose }: { expose: (value: { refresh: () => Promise<void> }) => void }) => {
|
||||
expose({ refresh: mocks.refreshEnhancedWorkspace });
|
||||
return () =>
|
||||
h("div", {
|
||||
"data-testid": "nacos-role-access-control-workspace",
|
||||
"data-can-create-user": String(props.capabilities.createUser.supported),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -98,6 +107,39 @@ describe("NacosAccessControlConsole", () => {
|
||||
expect(legacySource).not.toContain('<textarea v-model="userForm.blacklist"');
|
||||
});
|
||||
|
||||
it("invalidates the r-nacos namespace picker when namespaces change", () => {
|
||||
expect(legacySource).toContain('import { subscribeNacosNamespacesChanged, type NacosNamespacesChangedDetail } from "@/lib/nacos/nacosNamespaceCache"');
|
||||
expect(legacySource).toContain("stopNacosNamespacesChangedListener = subscribeNacosNamespacesChanged(handleNacosNamespacesChanged)");
|
||||
expect(legacySource).toContain("if (detail.connectionId !== props.connectionId) return");
|
||||
expect(legacySource).toContain("if (userDialogOpen.value && isRNacos.value) void loadUserFormNamespaces(true)");
|
||||
expect(legacySource).toContain("stopNacosNamespacesChangedListener?.()");
|
||||
});
|
||||
|
||||
it("notifies users when an official Nacos password reset succeeds or fails", () => {
|
||||
expect(legacySource).toContain('const userOperationNotice = ref<{ kind: "success" | "error"; message: string } | null>(null)');
|
||||
expect(legacySource).toContain("await syncCurrentConnectionPassword(username, userForm.value.password)");
|
||||
expect(legacySource).toContain('userOperationNotice.value = { kind: "error", message: t("nacos.passwordResetFailed", { message }) }');
|
||||
expect(legacySource).toContain('v-if="userOperationNotice"');
|
||||
});
|
||||
|
||||
it("requires exact username confirmation before deleting a user in the legacy workspace", () => {
|
||||
expect(legacySource).toContain("const userDeleteConfirmed = computed(() => userDeleteConfirmation.value === pendingUserDelete.value?.username)");
|
||||
expect(legacySource).toContain('@click="openDeleteUser(user)"');
|
||||
expect(legacySource).toContain('t("nacos.accessDeleteUserConfirmationLabel")');
|
||||
expect(legacySource).toContain(':disabled="deletingUser || !userDeleteConfirmed"');
|
||||
});
|
||||
|
||||
it("does not hide user deletion solely because the user has ROLE_ADMIN", () => {
|
||||
expect(legacySource).not.toContain('if (user.roles.includes("ROLE_ADMIN")) return');
|
||||
expect(legacySource).not.toContain("v-if=\"!user.roles.includes('ROLE_ADMIN')\"");
|
||||
});
|
||||
|
||||
it("uses the same role creation icon in the legacy and enhanced workspaces", () => {
|
||||
expect(legacySource).toContain('import { KeyRound, Loader2, Pencil, Plus, RefreshCw, Search, ShieldCheck, ShieldPlus, Trash2, UserRound } from "@lucide/vue"');
|
||||
expect(legacySource).toContain('@click="openAssignRole"><ShieldPlus class="h-3.5 w-3.5" />{{ t("nacos.assignRole") }}');
|
||||
expect(readFileSync(resolve(process.cwd(), "apps/desktop/src/components/nacos/NacosRoleAccessControl.vue"), "utf8")).toContain('<ShieldPlus v-else class="h-3.5 w-3.5" />');
|
||||
});
|
||||
|
||||
it("switches tabs when navigating between associated users and roles", () => {
|
||||
expect(source).toContain("@select-user=\"activeTab = 'users'\"");
|
||||
expect(source).toContain("@select-role=\"activeTab = 'roles'\"");
|
||||
@@ -202,6 +244,53 @@ describe("NacosAccessControlConsole", () => {
|
||||
expect(workspace?.getAttribute("data-can-create-user")).toBe("false");
|
||||
});
|
||||
|
||||
it("refreshes the enhanced workspace snapshot after refreshing connection capabilities", async () => {
|
||||
const supported = { supported: true };
|
||||
mocks.connectionInfo = {
|
||||
serverAddr: "http://127.0.0.1:8848",
|
||||
displayServerAddr: "http://127.0.0.1:8848",
|
||||
namespace: "",
|
||||
auth: "usernamePassword",
|
||||
capabilities: {
|
||||
supportsConfigManagement: true,
|
||||
supportsServiceManagement: true,
|
||||
supportsInstanceUpdate: true,
|
||||
supportsRawApi: true,
|
||||
accessControl: {
|
||||
mode: "roleBindings",
|
||||
listUsers: supported,
|
||||
createUser: supported,
|
||||
updateUser: supported,
|
||||
deleteUser: supported,
|
||||
listRoleBindings: supported,
|
||||
assignRole: supported,
|
||||
removeRole: supported,
|
||||
listPermissions: supported,
|
||||
grantPermission: supported,
|
||||
revokePermission: supported,
|
||||
enhancedWorkspace: true,
|
||||
supportsNamespacePrivileges: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp(NacosAccessControlConsole, { connectionId: "nacos-refresh" });
|
||||
mountedApps.push({ app, host });
|
||||
app.mount(host);
|
||||
await settle();
|
||||
mocks.refreshEnhancedWorkspace.mockClear();
|
||||
|
||||
const refreshButton = [...host.querySelectorAll("button")].find((button) => button.textContent?.includes("nacos.refresh"));
|
||||
expect(refreshButton).toBeDefined();
|
||||
refreshButton?.click();
|
||||
await settle();
|
||||
|
||||
expect(mocks.nacosTestConnection).toHaveBeenLastCalledWith("nacos-refresh", true);
|
||||
expect(mocks.refreshEnhancedWorkspace).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("internationalizes the sidebar entry, including trees persisted with the old label", () => {
|
||||
expect(connectionStoreSource).toContain('label: "nacos.accessControlSidebarLabel"');
|
||||
expect(treeItemSource).toContain('if (node.type === "nacos-access-control") return t("nacos.accessControlSidebarLabel")');
|
||||
|
||||
@@ -197,6 +197,12 @@ describe("NacosAdminConsole config workbench layout", () => {
|
||||
expect(source).toContain("border-destructive/50 text-destructive hover:bg-destructive/10");
|
||||
});
|
||||
|
||||
it("hides instance operations that the selected API plane does not support", () => {
|
||||
expect(source).toContain('v-if="registerInstanceCapability.supported"');
|
||||
expect(source).toContain('v-if="updateInstanceHealthCapability.supported"');
|
||||
expect(source).toContain('v-if="deregisterInstanceCapability.supported"');
|
||||
});
|
||||
|
||||
it("separates the service header, filtering controls, and management actions", () => {
|
||||
expect(source).toContain('<header class="shrink-0 border-b bg-background">');
|
||||
expect(source).toContain('class="flex flex-wrap items-center gap-x-4 gap-y-2 border-t bg-muted/30 px-4 py-2"');
|
||||
|
||||
+64
-2
@@ -10,6 +10,15 @@ describe("NacosRoleAccessControl", () => {
|
||||
expect(source).toContain("v-else-if=\"tab === 'roles' && selectedRole\"");
|
||||
});
|
||||
|
||||
it("places the contextual create action beside search instead of duplicating refresh", () => {
|
||||
expect(source).toContain(":aria-label=\"tab === 'users' ? t('nacos.createUser') : t('nacos.accessCreateRole')\"");
|
||||
expect(source).toContain("@click=\"tab === 'users' ? openCreateUser() : openCreateRole()\"");
|
||||
expect(source).toContain("<UserPlus v-if=\"tab === 'users'\"");
|
||||
expect(source).toContain("<ShieldPlus v-else");
|
||||
expect(source).not.toContain(":aria-label=\"t('nacos.refresh')\"");
|
||||
expect(source).not.toContain('class="h-8 w-full"');
|
||||
});
|
||||
|
||||
it("keeps role creation complete with per-namespace permissions", () => {
|
||||
expect(source).toContain("roleForm.newUsers");
|
||||
expect(source).toContain("moveNamespacesToGranted");
|
||||
@@ -20,6 +29,52 @@ describe("NacosRoleAccessControl", () => {
|
||||
expect(source).toContain('kind: "createRole"');
|
||||
});
|
||||
|
||||
it("keeps the selected role identity when opening the delete dialog", () => {
|
||||
expect(source).toContain("resetRoleForm(selectedRole.value.role)");
|
||||
});
|
||||
|
||||
it("makes the exact role-name confirmation requirement explicit before deletion", () => {
|
||||
expect(source).toContain("const roleDeleteConfirmed = computed(() => roleForm.confirmation === roleForm.role)");
|
||||
expect(source).toContain('t("nacos.accessDeleteRoleConfirmationLabel")');
|
||||
expect(source).toContain('t("nacos.accessDeleteRoleConfirmationHint")');
|
||||
expect(source).toContain("roleDialog === 'delete' && !roleDeleteConfirmed");
|
||||
});
|
||||
|
||||
it("requires the exact username before deleting any user", () => {
|
||||
expect(source).toContain("const userDeleteConfirmed = computed(() => userForm.confirmation === userForm.username)");
|
||||
expect(source).toContain('t("nacos.accessDeleteUserConfirmationLabel")');
|
||||
expect(source).toContain('t("nacos.accessDeleteUserConfirmationHint")');
|
||||
expect(source).toContain("userDialog === 'delete' && !userDeleteConfirmed");
|
||||
});
|
||||
|
||||
it("lets the backend workflow protect only the final ROLE_ADMIN member", () => {
|
||||
expect(source).toContain("if (!selectedUser.value) return");
|
||||
expect(source).not.toContain('selectedUser.value.roles.includes("ROLE_ADMIN")');
|
||||
expect(source).not.toContain("v-if=\"!selectedUser.roles.includes('ROLE_ADMIN')\"");
|
||||
});
|
||||
|
||||
it("shows complete IDs only in the unassigned namespace list", () => {
|
||||
expect(source).toContain('class="block truncate font-medium" :title="namespace.name"');
|
||||
expect(source).toContain('class="block break-all text-[10px] leading-4 text-muted-foreground" :title="namespace.id"');
|
||||
expect(source).not.toContain('<code v-if="namespace.name !== namespace.namespaceId"');
|
||||
});
|
||||
|
||||
it("uses a wider, internally scrollable namespace picker and filters by name or ID", () => {
|
||||
expect(source).toContain('class="max-h-[90vh] overflow-auto sm:max-w-3xl"');
|
||||
expect(source).toContain("h-[min(28rem,45vh)]");
|
||||
expect(source).toContain('const id = item.id ?? item.namespaceId ?? ""');
|
||||
expect(source).toContain("item.name.toLocaleLowerCase().includes(normalized) || id.toLocaleLowerCase().includes(normalized)");
|
||||
expect(source).toContain(":aria-label=\"t('nacos.accessSearchNamespaces')\"");
|
||||
});
|
||||
|
||||
it("refreshes the role namespace options after a namespace change for this connection", () => {
|
||||
expect(source).toContain('import { subscribeNacosNamespacesChanged, type NacosNamespacesChangedDetail } from "@/lib/nacos/nacosNamespaceCache"');
|
||||
expect(source).toContain("function handleNacosNamespacesChanged(detail: NacosNamespacesChangedDetail)");
|
||||
expect(source).toContain("if (detail.connectionId === props.connectionId) void loadSnapshot()");
|
||||
expect(source).toContain("stopNacosNamespacesChangedListener = subscribeNacosNamespacesChanged(handleNacosNamespacesChanged)");
|
||||
expect(source).toContain("stopNacosNamespacesChangedListener?.()");
|
||||
});
|
||||
|
||||
it("merges split permission rows before editing an existing role", () => {
|
||||
expect(source).toContain("mergeNacosNamespacePermissionAssignments(managedRolePermissions.value)");
|
||||
});
|
||||
@@ -30,8 +85,8 @@ describe("NacosRoleAccessControl", () => {
|
||||
expect(source).toContain("const canGrantPermission = computed");
|
||||
expect(source).toContain("const userOperationWritable = computed");
|
||||
expect(source).toContain("const roleOperationWritable = computed");
|
||||
expect(source).toContain(':disabled="saving || !userOperationWritable"');
|
||||
expect(source).toContain(':disabled="saving || !roleOperationWritable"');
|
||||
expect(source).toContain(':disabled="saving || !userOperationWritable ||');
|
||||
expect(source).toContain(':disabled="saving || !roleOperationWritable ||');
|
||||
});
|
||||
|
||||
it("surfaces partial operations instead of hiding them in a toast", () => {
|
||||
@@ -41,6 +96,13 @@ describe("NacosRoleAccessControl", () => {
|
||||
expect(source).toContain("accessOperationResult");
|
||||
});
|
||||
|
||||
it("uses the global notification for password reset results", () => {
|
||||
expect(source).toContain('import { useToast } from "@/composables/useToast"');
|
||||
expect(source).toContain("const { toast } = useToast()");
|
||||
expect(source).toContain('toast(t("nacos.passwordResetSucceeded", { username }), 2500)');
|
||||
expect(source).toContain('toast(t("nacos.passwordResetFailed", { message }), 5000)');
|
||||
});
|
||||
|
||||
it("renders associated roles with the same navigable card pattern as role members", () => {
|
||||
expect(source).toContain('v-if="selectedUser.roles.length" class="flex flex-wrap gap-1.5 rounded border bg-muted/20 p-1.5"');
|
||||
expect(source).toContain(":title=\"t('nacos.accessOpenRole', { role })\"");
|
||||
|
||||
@@ -95,6 +95,9 @@ const {
|
||||
editNacosNamespaceDesc,
|
||||
editNacosNamespaceLoading,
|
||||
confirmEditNacosNamespace,
|
||||
showDeleteNacosNamespaceConfirm,
|
||||
deleteNacosNamespaceLoading,
|
||||
confirmDeleteNacosNamespace,
|
||||
showRenameMongoCollectionDialog,
|
||||
renameMongoCollectionName,
|
||||
renameMongoCollectionError,
|
||||
@@ -195,6 +198,7 @@ watch(
|
||||
showEditDatabasePropertiesDialog,
|
||||
showCreateNacosNamespaceDialog,
|
||||
showEditNacosNamespaceDialog,
|
||||
showDeleteNacosNamespaceConfirm,
|
||||
showRenameMongoCollectionDialog,
|
||||
showCloneMongoCollectionDialog,
|
||||
showCreateMongoIndexDialog,
|
||||
@@ -741,6 +745,26 @@ watch(
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showDeleteNacosNamespaceConfirm">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("nacos.deleteNamespace") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm">
|
||||
<AlertTriangle class="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<p class="min-w-0 break-words text-muted-foreground [overflow-wrap:anywhere]">
|
||||
{{ t("nacos.deleteNamespaceDescription", { name: node.nacosNamespaceName || node.label, id: node.nacosNamespace || "public" }) }}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="deleteNacosNamespaceLoading" @click="showDeleteNacosNamespaceConfirm = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="destructive" :disabled="deleteNacosNamespaceLoading" @click="confirmDeleteNacosNamespace">
|
||||
{{ deleteNacosNamespaceLoading ? t("nacos.deletingNamespace") : t("dangerDialog.confirm") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showCreateSchemaDialog">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -254,6 +254,8 @@ import {
|
||||
editNacosNamespaceName,
|
||||
editNacosNamespaceDesc,
|
||||
editNacosNamespaceLoading,
|
||||
showDeleteNacosNamespaceConfirm,
|
||||
deleteNacosNamespaceLoading,
|
||||
createDatabaseCharsetOptions,
|
||||
createDatabaseCollationsByCharset,
|
||||
createDatabaseCharsetLoading,
|
||||
@@ -497,6 +499,8 @@ const {
|
||||
confirmCreateNacosNamespace,
|
||||
openEditNacosNamespaceDialog,
|
||||
confirmEditNacosNamespace,
|
||||
openDeleteNacosNamespaceConfirm,
|
||||
confirmDeleteNacosNamespace,
|
||||
dropMongoCollection,
|
||||
dropMilvusCollection,
|
||||
dropMongoIndex,
|
||||
@@ -2898,6 +2902,8 @@ const canEditNacosNamespace = computed(() => {
|
||||
return config?.db_type === "nacos" && !config.read_only;
|
||||
});
|
||||
|
||||
const canDeleteNacosNamespace = computed(() => canEditNacosNamespace.value);
|
||||
|
||||
const isDuckDbConnection = computed(() => {
|
||||
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
||||
return activeNode.value.type === "connection" && config?.db_type === "duckdb" && connectionNamespaceCreationTarget(config) === "attach";
|
||||
@@ -4450,6 +4456,9 @@ function databaseSpecificDialogCapabilities() {
|
||||
editNacosNamespaceDesc,
|
||||
editNacosNamespaceLoading,
|
||||
confirmEditNacosNamespace,
|
||||
showDeleteNacosNamespaceConfirm,
|
||||
deleteNacosNamespaceLoading,
|
||||
confirmDeleteNacosNamespace,
|
||||
showRenameMongoCollectionDialog,
|
||||
renameMongoCollectionName,
|
||||
renameMongoCollectionError,
|
||||
@@ -5114,6 +5123,16 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
});
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
if (canDeleteNacosNamespace.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: t("nacos.deleteNamespace"),
|
||||
action: openDeleteNacosNamespaceConfirm,
|
||||
icon: Trash2,
|
||||
shortcut: shortcutDelete,
|
||||
variant: "destructive" as const,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ export const showEditNacosNamespaceDialog = ref(false);
|
||||
export const editNacosNamespaceName = ref("");
|
||||
export const editNacosNamespaceDesc = ref("");
|
||||
export const editNacosNamespaceLoading = ref(false);
|
||||
export const showDeleteNacosNamespaceConfirm = ref(false);
|
||||
export const deleteNacosNamespaceLoading = ref(false);
|
||||
export const createDatabaseCharsetOptions = ref<string[]>(fallbackCreateDatabaseCharset.charsets);
|
||||
export const createDatabaseCollationsByCharset = ref<Record<string, string[]>>(fallbackCreateDatabaseCharset.collationsByCharset);
|
||||
export const createDatabaseCharsetLoading = ref(false);
|
||||
|
||||
Vendored
+136
-1
@@ -1,13 +1,30 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { shallowRef } from "vue";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { createNacosNamespaceDesc, createNacosNamespaceId, createNacosNamespaceLoading, createNacosNamespaceName, showCreateNacosNamespaceDialog, sidebarFormTarget } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
import {
|
||||
createNacosNamespaceDesc,
|
||||
createNacosNamespaceId,
|
||||
createNacosNamespaceLoading,
|
||||
createNacosNamespaceName,
|
||||
editNacosNamespaceDesc,
|
||||
editNacosNamespaceLoading,
|
||||
editNacosNamespaceName,
|
||||
deleteNacosNamespaceLoading,
|
||||
showCreateNacosNamespaceDialog,
|
||||
showDeleteNacosNamespaceConfirm,
|
||||
showEditNacosNamespaceDialog,
|
||||
sidebarDangerTarget,
|
||||
sidebarFormTarget,
|
||||
} from "@/components/sidebar/sidebarTreeDialogState";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toast: vi.fn(),
|
||||
nacosCreateNamespace: vi.fn(),
|
||||
nacosDeleteNamespace: vi.fn(),
|
||||
nacosUpdateNamespace: vi.fn(),
|
||||
notifyNacosNamespacesChanged: vi.fn(),
|
||||
loadNacosNamespaces: vi.fn(),
|
||||
updateConnection: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("vue-i18n", () => ({
|
||||
@@ -22,6 +39,12 @@ vi.mock("@/composables/useToast", () => ({
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
nacosCreateNamespace: (...args: unknown[]) => mocks.nacosCreateNamespace(...args),
|
||||
nacosDeleteNamespace: (...args: unknown[]) => mocks.nacosDeleteNamespace(...args),
|
||||
nacosUpdateNamespace: (...args: unknown[]) => mocks.nacosUpdateNamespace(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/database/productionExecutionGuard", () => ({
|
||||
executeWithProductionContextGuard: vi.fn(async ({ execute }: { execute: () => Promise<unknown> }) => execute()),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/nacos/nacosNamespaceCache", () => ({
|
||||
@@ -40,6 +63,17 @@ function connectionNode(): TreeNode {
|
||||
};
|
||||
}
|
||||
|
||||
function namespaceNode(): TreeNode {
|
||||
return {
|
||||
id: "conn-1:nacos:existing-space",
|
||||
label: "Existing Space",
|
||||
type: "nacos-namespace",
|
||||
connectionId: "conn-1",
|
||||
nacosNamespace: "existing-space",
|
||||
nacosNamespaceName: "Existing Space",
|
||||
};
|
||||
}
|
||||
|
||||
describe("Nacos namespace creation cache invalidation", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -94,3 +128,104 @@ describe("Nacos namespace creation cache invalidation", () => {
|
||||
expect(createNacosNamespaceLoading.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Nacos namespace edit cache invalidation", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.nacosUpdateNamespace.mockResolvedValue(undefined);
|
||||
mocks.loadNacosNamespaces.mockResolvedValue(undefined);
|
||||
sidebarFormTarget.value = namespaceNode();
|
||||
editNacosNamespaceName.value = "Renamed Space";
|
||||
editNacosNamespaceDesc.value = "Updated during sync";
|
||||
editNacosNamespaceLoading.value = false;
|
||||
showEditNacosNamespaceDialog.value = true;
|
||||
});
|
||||
|
||||
it("notifies open views before refreshing the sidebar after an edit", async () => {
|
||||
const { confirmEditNacosNamespace } = useSidebarDatabaseSpecificMutationRuntime({
|
||||
activeNode: shallowRef(namespaceNode()),
|
||||
connectionStore: {
|
||||
treeNodes: [],
|
||||
loadNacosNamespaces: mocks.loadNacosNamespaces,
|
||||
getConfig: () => undefined,
|
||||
} as any,
|
||||
});
|
||||
|
||||
await confirmEditNacosNamespace();
|
||||
|
||||
expect(mocks.nacosUpdateNamespace).toHaveBeenCalledWith("conn-1", {
|
||||
namespaceId: "existing-space",
|
||||
namespaceName: "Renamed Space",
|
||||
namespaceDesc: "Updated during sync",
|
||||
});
|
||||
expect(mocks.notifyNacosNamespacesChanged).toHaveBeenCalledWith("conn-1");
|
||||
expect(mocks.notifyNacosNamespacesChanged.mock.invocationCallOrder[0]).toBeLessThan(mocks.loadNacosNamespaces.mock.invocationCallOrder[0]);
|
||||
expect(showEditNacosNamespaceDialog.value).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps namespace caches intact when an edit fails", async () => {
|
||||
mocks.nacosUpdateNamespace.mockRejectedValueOnce(new Error("edit failed"));
|
||||
const { confirmEditNacosNamespace } = useSidebarDatabaseSpecificMutationRuntime({
|
||||
activeNode: shallowRef(namespaceNode()),
|
||||
connectionStore: {
|
||||
treeNodes: [],
|
||||
loadNacosNamespaces: mocks.loadNacosNamespaces,
|
||||
getConfig: () => undefined,
|
||||
} as any,
|
||||
});
|
||||
|
||||
await confirmEditNacosNamespace();
|
||||
|
||||
expect(mocks.notifyNacosNamespacesChanged).not.toHaveBeenCalled();
|
||||
expect(mocks.loadNacosNamespaces).not.toHaveBeenCalled();
|
||||
expect(showEditNacosNamespaceDialog.value).toBe(true);
|
||||
expect(editNacosNamespaceLoading.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Nacos namespace deletion scope rollback", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.nacosDeleteNamespace.mockRejectedValue(new Error("remote delete failed"));
|
||||
mocks.loadNacosNamespaces.mockResolvedValue(undefined);
|
||||
sidebarDangerTarget.value = namespaceNode();
|
||||
deleteNacosNamespaceLoading.value = false;
|
||||
showDeleteNacosNamespaceConfirm.value = true;
|
||||
});
|
||||
|
||||
it("restores the last managed namespace when the remote deletion fails", async () => {
|
||||
let currentConfig: any = {
|
||||
id: "conn-1",
|
||||
db_type: "nacos",
|
||||
visible_databases: ["existing-space"],
|
||||
external_config: {
|
||||
serverAddr: "http://127.0.0.1:8848",
|
||||
managedNamespaces: ["existing-space"],
|
||||
},
|
||||
};
|
||||
mocks.updateConnection.mockImplementation(async (config: any) => {
|
||||
currentConfig = config;
|
||||
});
|
||||
const { confirmDeleteNacosNamespace } = useSidebarDatabaseSpecificMutationRuntime({
|
||||
activeNode: shallowRef(namespaceNode()),
|
||||
connectionStore: {
|
||||
treeNodes: [],
|
||||
loadNacosNamespaces: mocks.loadNacosNamespaces,
|
||||
getConfig: () => currentConfig,
|
||||
updateConnection: mocks.updateConnection,
|
||||
} as any,
|
||||
});
|
||||
|
||||
await confirmDeleteNacosNamespace();
|
||||
|
||||
expect(mocks.nacosDeleteNamespace).toHaveBeenCalledWith("conn-1", "existing-space");
|
||||
expect(mocks.updateConnection).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.updateConnection.mock.calls[0][0].visible_databases).toEqual([]);
|
||||
expect(mocks.updateConnection.mock.calls[0][0].external_config.managedNamespaces).toEqual([]);
|
||||
expect(currentConfig.visible_databases).toEqual(["existing-space"]);
|
||||
expect(currentConfig.external_config.managedNamespaces).toEqual(["existing-space"]);
|
||||
expect(mocks.notifyNacosNamespacesChanged).not.toHaveBeenCalled();
|
||||
expect(showDeleteNacosNamespaceConfirm.value).toBe(true);
|
||||
expect(deleteNacosNamespaceLoading.value).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,9 @@ import {
|
||||
import { supportsMongoAllDriverMutations, supportsMongoIndexMutations, supportsNativeMongoDriverMutations } from "@/lib/mongo/mongoCapabilities";
|
||||
import { runMongoSidebarMutation } from "@/lib/sidebar/runMongoSidebarMutation";
|
||||
import { executeWithProductionContextGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata";
|
||||
import type { NacosAdminConfig } from "@/types/nacos";
|
||||
import {
|
||||
sidebarDangerTarget,
|
||||
sidebarFormTarget,
|
||||
@@ -48,6 +50,8 @@ import {
|
||||
editNacosNamespaceName,
|
||||
editNacosNamespaceDesc,
|
||||
editNacosNamespaceLoading,
|
||||
showDeleteNacosNamespaceConfirm,
|
||||
deleteNacosNamespaceLoading,
|
||||
showDropMongoCollectionConfirm,
|
||||
dropMongoCollectionLoading,
|
||||
showDropMongoIndexConfirm,
|
||||
@@ -668,11 +672,70 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
||||
showCreateNacosNamespaceDialog.value = true;
|
||||
}
|
||||
|
||||
interface ExplicitNacosNamespaceScopeSnapshot {
|
||||
visibleDatabases?: string[];
|
||||
managedNamespaces?: string[];
|
||||
}
|
||||
|
||||
function snapshotExplicitNacosNamespaceScope(connectionId: string): ExplicitNacosNamespaceScopeSnapshot {
|
||||
const config = connectionStore.getConfig(connectionId);
|
||||
if (!config || config.db_type !== "nacos") return {};
|
||||
const externalConfig = (config.external_config || {}) as NacosAdminConfig;
|
||||
return {
|
||||
visibleDatabases: Array.isArray(config.visible_databases) ? [...config.visible_databases] : undefined,
|
||||
managedNamespaces: Array.isArray(externalConfig.managedNamespaces) && externalConfig.managedNamespaces.length > 0 ? [...externalConfig.managedNamespaces] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function restoreExplicitNacosNamespaceScope(connectionId: string, snapshot: ExplicitNacosNamespaceScopeSnapshot): Promise<void> {
|
||||
if (!snapshot.visibleDatabases && !snapshot.managedNamespaces) return;
|
||||
const config = connectionStore.getConfig(connectionId);
|
||||
if (!config || config.db_type !== "nacos") return;
|
||||
const externalConfig = (config.external_config || {}) as NacosAdminConfig;
|
||||
await connectionStore.updateConnection({
|
||||
...config,
|
||||
visible_databases: snapshot.visibleDatabases ? [...snapshot.visibleDatabases] : config.visible_databases,
|
||||
external_config: snapshot.managedNamespaces ? { ...externalConfig, managedNamespaces: [...snapshot.managedNamespaces] } : externalConfig,
|
||||
});
|
||||
}
|
||||
|
||||
async function updateExplicitNacosNamespaceScope(connectionId: string, namespaceId: string, action: "add" | "remove"): Promise<boolean> {
|
||||
const config = connectionStore.getConfig(connectionId);
|
||||
if (!config || config.db_type !== "nacos") return false;
|
||||
const externalConfig = (config.external_config || {}) as NacosAdminConfig;
|
||||
const hasVisibleScope = Array.isArray(config.visible_databases);
|
||||
const hasManagedScope = Array.isArray(externalConfig.managedNamespaces) && externalConfig.managedNamespaces.length > 0;
|
||||
if (!hasVisibleScope && !hasManagedScope) return false;
|
||||
|
||||
const mutate = (values: string[] | undefined) => {
|
||||
const normalized = new Set((values || []).map((value) => value.trim()).filter(Boolean));
|
||||
if (action === "add") normalized.add(namespaceId);
|
||||
else normalized.delete(namespaceId);
|
||||
return [...normalized];
|
||||
};
|
||||
await connectionStore.updateConnection({
|
||||
...config,
|
||||
visible_databases: hasVisibleScope ? mutate(config.visible_databases) : config.visible_databases,
|
||||
external_config: hasManagedScope ? { ...externalConfig, managedNamespaces: mutate(externalConfig.managedNamespaces) } : externalConfig,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async function refreshNacosNamespacesAfterMutation(connectionId: string) {
|
||||
try {
|
||||
await connectionStore.loadNacosNamespaces(connectionId, { force: true });
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
toast(translateBackendError(t, error), 5000);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmCreateNacosNamespace() {
|
||||
const node = sidebarFormTarget.value ?? activeNode.value;
|
||||
const namespaceName = createNacosNamespaceName.value.trim();
|
||||
if (!node.connectionId || !namespaceName || createNacosNamespaceLoading.value) return;
|
||||
const namespaceId = createNacosNamespaceId.value.trim();
|
||||
const namespaceId = createNacosNamespaceId.value.trim() || uuid();
|
||||
const confirmed = await executeWithProductionContextGuard({
|
||||
connection: connectionStore.getConfig(node.connectionId),
|
||||
database: namespaceId || undefined,
|
||||
@@ -684,16 +747,22 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
||||
createNacosNamespaceLoading.value = true;
|
||||
try {
|
||||
await api.nacosCreateNamespace(node.connectionId, {
|
||||
namespaceId: namespaceId || undefined,
|
||||
namespaceId,
|
||||
namespaceName,
|
||||
namespaceDesc: createNacosNamespaceDesc.value.trim() || namespaceName,
|
||||
});
|
||||
try {
|
||||
await updateExplicitNacosNamespaceScope(node.connectionId, namespaceId, "add");
|
||||
} catch (error: any) {
|
||||
toast(t("connection.saveFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
}
|
||||
notifyNacosNamespacesChanged(node.connectionId);
|
||||
showCreateNacosNamespaceDialog.value = false;
|
||||
await connectionStore.loadNacosNamespaces(node.connectionId, { force: true });
|
||||
const liveNode = findSidebarActionTarget(connectionStore.treeNodes, node);
|
||||
if (liveNode) liveNode.isExpanded = true;
|
||||
toast(t("nacos.namespaceCreated", { name: namespaceName }), 3000);
|
||||
if (await refreshNacosNamespacesAfterMutation(node.connectionId)) {
|
||||
const liveNode = findSidebarActionTarget(connectionStore.treeNodes, node);
|
||||
if (liveNode) liveNode.isExpanded = true;
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
} finally {
|
||||
@@ -727,9 +796,10 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
||||
namespaceName,
|
||||
namespaceDesc: editNacosNamespaceDesc.value.trim() || namespaceName,
|
||||
});
|
||||
notifyNacosNamespacesChanged(node.connectionId);
|
||||
showEditNacosNamespaceDialog.value = false;
|
||||
await connectionStore.loadNacosNamespaces(node.connectionId, { force: true });
|
||||
toast(t("nacos.namespaceUpdated", { name: namespaceName }), 3000);
|
||||
await refreshNacosNamespacesAfterMutation(node.connectionId);
|
||||
} catch (error: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
} finally {
|
||||
@@ -737,6 +807,50 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteNacosNamespaceConfirm() {
|
||||
deleteNacosNamespaceLoading.value = false;
|
||||
showDeleteNacosNamespaceConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmDeleteNacosNamespace() {
|
||||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
const namespaceId = node.nacosNamespace?.trim() || "";
|
||||
if (node.type !== "nacos-namespace" || !node.connectionId || !namespaceId || deleteNacosNamespaceLoading.value) return;
|
||||
const confirmed = await executeWithProductionContextGuard({
|
||||
connection: connectionStore.getConfig(node.connectionId),
|
||||
database: namespaceId,
|
||||
reviewText: `${t("nacos.deleteNamespace")}: ${node.nacosNamespaceName || node.label}`,
|
||||
source: t("production.sourceSidebar"),
|
||||
execute: async () => true,
|
||||
});
|
||||
if (confirmed !== true) return;
|
||||
deleteNacosNamespaceLoading.value = true;
|
||||
const scopeSnapshot = snapshotExplicitNacosNamespaceScope(node.connectionId);
|
||||
let scopeUpdated = false;
|
||||
try {
|
||||
// Persist the local scope first. A local save failure must abort before
|
||||
// the irreversible remote deletion. If Nacos rejects the deletion, the
|
||||
// original scope is restored below.
|
||||
scopeUpdated = await updateExplicitNacosNamespaceScope(node.connectionId, namespaceId, "remove");
|
||||
await api.nacosDeleteNamespace(node.connectionId, namespaceId);
|
||||
notifyNacosNamespacesChanged(node.connectionId);
|
||||
showDeleteNacosNamespaceConfirm.value = false;
|
||||
toast(t("nacos.namespaceDeleted", { name: node.nacosNamespaceName || node.label }), 3000);
|
||||
await refreshNacosNamespacesAfterMutation(node.connectionId);
|
||||
} catch (error: any) {
|
||||
if (scopeUpdated) {
|
||||
try {
|
||||
await restoreExplicitNacosNamespaceScope(node.connectionId, scopeSnapshot);
|
||||
} catch (rollbackError: any) {
|
||||
toast(t("connection.saveFailed", { message: translateBackendError(t, rollbackError) }), 5000);
|
||||
}
|
||||
}
|
||||
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, error) }), 5000);
|
||||
} finally {
|
||||
deleteNacosNamespaceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function dropMongoCollection() {
|
||||
dropMongoCollectionLoading.value = false;
|
||||
showDropMongoCollectionConfirm.value = true;
|
||||
@@ -1047,6 +1161,8 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
||||
confirmCreateNacosNamespace,
|
||||
openEditNacosNamespaceDialog,
|
||||
confirmEditNacosNamespace,
|
||||
openDeleteNacosNamespaceConfirm,
|
||||
confirmDeleteNacosNamespace,
|
||||
dropMongoCollection,
|
||||
dropMongoIndex,
|
||||
dropAllMongoIndexes,
|
||||
|
||||
@@ -7961,6 +7961,11 @@ export default {
|
||||
createUser: "Create user",
|
||||
editUser: "Edit user",
|
||||
resetUserPassword: "Reset password",
|
||||
passwordResetSucceeded: "Reset password for {username}",
|
||||
passwordResetFailed: "Failed to reset password: {message}",
|
||||
currentPasswordSaved: "The saved connection credential was updated and the connection will use the new password.",
|
||||
currentSessionPasswordUpdated: "The in-memory connection credential was updated for this session and was not saved to disk.",
|
||||
currentPasswordNotSaved: "This connection does not save passwords. Enter the new password when reconnecting.",
|
||||
deleteUser: "Delete user",
|
||||
noUsers: "No users found",
|
||||
noRoleBindings: "No role bindings found",
|
||||
@@ -8014,7 +8019,7 @@ export default {
|
||||
accessGlobalAdministrator: "Global administrator",
|
||||
accessNeedsRepair: "Needs repair",
|
||||
accessRoleSubtitle: "{members} associated members",
|
||||
accessIncompleteRoleHint: "This historical role has no members or namespace permissions. Add both before saving changes.",
|
||||
accessIncompleteRoleHint: "This role has no members or namespace permissions. Add namespace permissions before saving; members may remain empty.",
|
||||
accessPermissionScope: "Permission scope",
|
||||
accessPermissionScopeHint: "Namespace rules managed by this role.",
|
||||
accessAllResources: "All resources",
|
||||
@@ -8043,19 +8048,26 @@ export default {
|
||||
accessInlineUserRequired: "Every new user requires a username and password.",
|
||||
accessNoAssignableRoles: "No complete roles are available",
|
||||
accessTypeUsername: "Type {value} to confirm this high-risk account change",
|
||||
accessDeleteUserConfirmationLabel: "Enter the complete username to confirm deletion",
|
||||
accessDeleteUserConfirmationHint: "Enter this text exactly (case-sensitive):",
|
||||
accessDeleteUserConfirmationPlaceholder: "Complete username",
|
||||
accessMembersRequiredHint: "Select existing users or create users inline. At least one is required.",
|
||||
accessMembersOptionalHint: "An existing ordinary role may keep its permissions without any associated members.",
|
||||
accessInlineCreateUser: "Create user inline",
|
||||
accessAdminPermissionImmutable: "The global administrator permission is built into Nacos and cannot be edited as namespace rules.",
|
||||
accessPermissionRequiredHint: "Select namespaces, then set the action for each one.",
|
||||
accessUnassignedNamespaces: "Unassigned namespaces",
|
||||
accessGrantedNamespaces: "Granted namespaces",
|
||||
accessSearchNamespaces: "Search namespaces",
|
||||
accessSearchNamespaces: "Search by namespace name or ID",
|
||||
accessNoUnassignedNamespaces: "No namespaces available to add",
|
||||
accessNoGrantedNamespaces: "Select namespaces to grant access",
|
||||
accessGrantNamespaces: "Grant selected namespaces",
|
||||
accessRevokeNamespaces: "Remove selected namespaces",
|
||||
accessDeleteRoleHint: "Deleting {role} removes every permission and member binding. Successful deletions are not restored after a partial failure.",
|
||||
accessTypeRole: "Type {value} to confirm role deletion",
|
||||
accessDeleteRoleConfirmationLabel: "Enter the complete role name to confirm deletion",
|
||||
accessDeleteRoleConfirmationHint: "Enter this text exactly (case-sensitive):",
|
||||
accessDeleteRoleConfirmationPlaceholder: "Complete role name",
|
||||
accessOperationResult: "Operation result",
|
||||
accessReenterPassword: "Enter the password again to retry",
|
||||
accessUndoCreated: "Undo created items",
|
||||
@@ -8099,12 +8111,16 @@ export default {
|
||||
namespace: "Namespace",
|
||||
createNamespace: "Create Namespace",
|
||||
editNamespace: "Edit Namespace",
|
||||
deleteNamespace: "Delete Namespace",
|
||||
deleteNamespaceDescription: "Delete namespace {name} ({id})? Its configurations and services may become inaccessible. This cannot be undone.",
|
||||
creatingNamespace: "Creating...",
|
||||
updatingNamespace: "Updating...",
|
||||
deletingNamespace: "Deleting...",
|
||||
namespaceCreated: 'Namespace "{name}" created',
|
||||
namespaceUpdated: 'Namespace "{name}" updated',
|
||||
namespaceDeleted: 'Namespace "{name}" deleted',
|
||||
namespaceId: "Namespace ID",
|
||||
namespaceIdPlaceholder: "Leave empty for Nacos to generate",
|
||||
namespaceIdPlaceholder: "Leave empty for DBX to generate a UUID",
|
||||
namespaceName: "Namespace Name",
|
||||
namespaceNamePlaceholder: "Enter a namespace name",
|
||||
namespaceDesc: "Description",
|
||||
@@ -8335,10 +8351,18 @@ export default {
|
||||
readOnly: "Read only",
|
||||
nacosConnectionPlan: "Connection plan",
|
||||
nacosConnectionPlanDescription: "Choose the service implementation and API version.",
|
||||
nacosApiPlane: "Nacos 3 API",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "Recommended · full management",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "Restricted · works with console-only access",
|
||||
nacosServiceAddress: "Service address",
|
||||
nacosServiceAddressHint: "Enter the service address. The default port is 8848, for example http://host:8848/nacos.",
|
||||
nacosConsoleServiceAddressHint: "Enter the Console API address. The default port is 8080, for example http://host:8080.",
|
||||
nacosManagedNamespaces: "Managed namespace IDs",
|
||||
nacosManagedNamespacesNameOrId: "Managed namespace names or IDs",
|
||||
nacosManagedNamespacesPlaceholder: "For example: public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "For example: public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "Enter the namespace IDs this ordinary user can access, separated by commas or new lines. The connection test verifies configuration and service read access for each ID.",
|
||||
nacosOrdinaryAccount: "Ordinary user",
|
||||
nacosOrdinaryAccountHint: "If the account above is an ordinary user, enable this option and select its authorized namespaces; otherwise the connection will not work correctly.",
|
||||
@@ -8346,6 +8370,16 @@ export default {
|
||||
nacosOrdinaryNamespacesRequired: "Enter at least one namespace ID that this ordinary user is allowed to manage.",
|
||||
nacosManagedNamespacesRequired: "This Nacos ordinary user cannot list namespaces or authorization data. Enter at least one namespace ID it is allowed to manage and try again.",
|
||||
nacosManagedNamespaceAccessDenied: "These namespaces failed the configuration or service read-access check: {detail}",
|
||||
nacosNamespaceAccessScope: "Namespace access scope",
|
||||
nacosNamespaceAccessScopeHint: "Detect accessible namespaces first. If enumeration is unavailable, enter verified namespace IDs manually.",
|
||||
nacosNamespaceAccessAutomatic: "Detect automatically",
|
||||
nacosNamespaceAccessManual: "Enter manually",
|
||||
nacosDetectAccessibleNamespaces: "Detect and select accessible namespaces",
|
||||
nacosDetectAccessibleNamespacesHint: "DBX verifies configuration and service read access before offering namespaces for {name}.",
|
||||
nacosV3AdminNamespaceScopeHint: "Nacos 3 Admin APIs normally require an administrator account. Use the Console API plane for a restricted account and its namespace access scope.",
|
||||
nacosV3AdminManagedNamespacesHint: "This uses the Nacos 3 Admin API (normally port 8848). An ordinary user must enter namespace IDs, not display names. Copy the ID from Nacos Console namespace details or role-permission resources.",
|
||||
nacosV3ConsoleManagedNamespacesHint: "This uses the Nacos 3 Console API (normally port 8080). Enter a namespace name or ID; DBX converts a name to its actual ID when it can read the directory.",
|
||||
rnacosNamespaceAccessScopeHint: "r-nacos derives namespace access from the current user's namespace privileges when its console profile is available.",
|
||||
nacosAuth: "Authentication",
|
||||
nacosAuthHint: "These credentials are used for connection tests and daily management.",
|
||||
nacosUsernamePassword: "Username / password",
|
||||
@@ -8353,8 +8387,10 @@ export default {
|
||||
nacosAdvancedHintDescription: "Configure Prometheus metrics, page size, TLS verification, and r-nacos extensions there.",
|
||||
nacosGoAdvanced: "Go to Advanced",
|
||||
nacosV3ConsolePortWarning: "8080 is the default Nacos 3 Web Console port. Use the Server / Admin API address on port 8848 for service management, for example http://127.0.0.1:8848/nacos.",
|
||||
nacosV3AdminPortWarning: "8848 is normally the Admin API port. For Console API mode, use the Console address, normally http://127.0.0.1:8080.",
|
||||
nacosAdvancedTitle: "Nacos advanced connection options",
|
||||
nacosAdvancedDescription: "These settings have safe defaults and usually only need adjustment for custom deployments, metrics, or r-nacos extensions.",
|
||||
nacosContextPathHint: "Leave empty to use the selected profile default. Enter / to explicitly use the server root, or provide a reverse-proxy prefix such as /nacos.",
|
||||
nacosPageSizeHint: "Used for configuration and service lists; 1–500 items per page.",
|
||||
nacosMetricsHint: "Query metrics from the service address automatically; choose a custom URL when the metrics endpoint differs.",
|
||||
nacosRnacosExtension: "r-nacos console features",
|
||||
|
||||
@@ -6577,7 +6577,7 @@ export default withEnglishFallback({
|
||||
namespaceCreated: 'Namespace "{name}" creado',
|
||||
namespaceUpdated: 'Namespace "{name}" actualizado',
|
||||
namespaceId: "ID del Namespace",
|
||||
namespaceIdPlaceholder: "Déjalo vacío para que Nacos lo genere",
|
||||
namespaceIdPlaceholder: "Déjalo vacío para que DBX genere un UUID",
|
||||
namespaceName: "Nombre del Namespace",
|
||||
namespaceNamePlaceholder: "Ingresa un nombre de namespace",
|
||||
namespaceDesc: "Descripción",
|
||||
@@ -6808,10 +6808,18 @@ export default withEnglishFallback({
|
||||
readOnly: "Solo lectura",
|
||||
nacosConnectionPlan: "Plan de conexión",
|
||||
nacosConnectionPlanDescription: "Elige la implementación del servicio y la versión de la API.",
|
||||
nacosApiPlane: "Modo de API de Nacos 3",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "Recomendado · gestión completa",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "Limitado · para acceso solo a la consola",
|
||||
nacosServiceAddress: "Dirección del servicio",
|
||||
nacosServiceAddressHint: "Introduce la dirección del servicio. El puerto predeterminado es 8848, por ejemplo http://host:8848/nacos.",
|
||||
nacosConsoleServiceAddressHint: "Introduce la dirección de Console API. El puerto predeterminado es 8080, por ejemplo http://host:8080.",
|
||||
nacosManagedNamespaces: "ID de espacios administrados",
|
||||
nacosManagedNamespacesNameOrId: "Nombres o ID de espacios administrados",
|
||||
nacosManagedNamespacesPlaceholder: "Por ejemplo: public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "Por ejemplo: public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "Introduzca los ID accesibles para este usuario normal, separados por comas o líneas. La prueba de conexión valida la lectura de configuraciones y servicios para cada ID.",
|
||||
nacosOrdinaryAccount: "Usuario normal",
|
||||
nacosOrdinaryAccountHint: "Si la cuenta anterior es un usuario normal, active esta opción y seleccione sus espacios autorizados; de lo contrario, la conexión no funcionará correctamente.",
|
||||
@@ -6819,6 +6827,16 @@ export default withEnglishFallback({
|
||||
nacosOrdinaryNamespacesRequired: "Introduzca al menos un ID de espacio que este usuario normal pueda administrar.",
|
||||
nacosManagedNamespacesRequired: "Este usuario normal de Nacos no puede listar los espacios ni los datos de autorización. Introduzca al menos un ID que tenga permiso para administrar y vuelva a intentarlo.",
|
||||
nacosManagedNamespaceAccessDenied: "Estos espacios no superaron la comprobación de lectura de configuraciones o servicios: {detail}",
|
||||
nacosNamespaceAccessScope: "Alcance de acceso a namespaces",
|
||||
nacosNamespaceAccessScopeHint: "Primero detecta los namespaces accesibles. Si no se pueden enumerar, introduce manualmente IDs verificados.",
|
||||
nacosNamespaceAccessAutomatic: "Detectar automáticamente",
|
||||
nacosNamespaceAccessManual: "Introducir manualmente",
|
||||
nacosDetectAccessibleNamespaces: "Detectar y seleccionar namespaces accesibles",
|
||||
nacosDetectAccessibleNamespacesHint: "DBX verifica el acceso de lectura a configuraciones y servicios antes de ofrecer namespaces para {name}.",
|
||||
nacosV3AdminNamespaceScopeHint: "Las API Admin de Nacos 3 suelen requerir una cuenta administradora. Para una cuenta restringida, usa el plano Console API.",
|
||||
nacosV3AdminManagedNamespacesHint: "Esta conexión usa la API Admin de Nacos 3 (normalmente el puerto 8848). Un usuario normal debe introducir ID de namespace, no nombres visibles. Copia el ID desde los detalles del namespace o los permisos de rol en Nacos Console.",
|
||||
nacosV3ConsoleManagedNamespacesHint: "Esta conexión usa la API Console de Nacos 3 (normalmente el puerto 8080). Introduce un nombre o ID de namespace; DBX convierte el nombre al ID real cuando puede leer el directorio.",
|
||||
rnacosNamespaceAccessScopeHint: "r-nacos obtiene el acceso a namespaces de los privilegios del usuario actual cuando su perfil de consola está disponible.",
|
||||
nacosAuth: "Autenticación",
|
||||
nacosAuthHint: "Estas credenciales se usan para probar la conexión y para la administración diaria.",
|
||||
nacosUsernamePassword: "Usuario / contraseña",
|
||||
@@ -6826,6 +6844,7 @@ export default withEnglishFallback({
|
||||
nacosAdvancedHintDescription: "Configura allí las métricas de Prometheus, el tamaño de página, la validación TLS y las extensiones de r-nacos.",
|
||||
nacosGoAdvanced: "Ir a Avanzado",
|
||||
nacosV3ConsolePortWarning: "8080 es el puerto predeterminado de la consola web de Nacos 3. Para administrar servicios, usa la dirección Server / Admin API del puerto 8848, por ejemplo http://127.0.0.1:8848/nacos.",
|
||||
nacosV3AdminPortWarning: "8848 suele ser el puerto de Admin API. Para Console API, usa la dirección de la consola, normalmente http://127.0.0.1:8080.",
|
||||
nacosAdvancedTitle: "Opciones avanzadas de conexión de Nacos",
|
||||
nacosAdvancedDescription: "Estos parámetros tienen valores seguros y normalmente solo deben ajustarse para despliegues personalizados, métricas o extensiones de r-nacos.",
|
||||
nacosPageSizeHint: "Se usa para las listas de configuraciones y servicios; entre 1 y 500 elementos por página.",
|
||||
@@ -6910,6 +6929,7 @@ export default withEnglishFallback({
|
||||
restore: "Restaurar",
|
||||
registerInstance: "Registrar instancia",
|
||||
deregister: "Dar de baja",
|
||||
nacosContextPathHint: "Cuando se deja vacío, se usa el valor predeterminado del modo de conexión actual; si se usa explícitamente la ruta raíz del servicio, complete /; en escenarios de proxy inverso, puede completar prefijos como /nacos.",
|
||||
},
|
||||
databaseBrowser: {
|
||||
search: "Buscar bases de datos...",
|
||||
|
||||
@@ -6578,7 +6578,7 @@ export default withEnglishFallback({
|
||||
namespaceCreated: 'Namespace "{name}" creato',
|
||||
namespaceUpdated: 'Namespace "{name}" aggiornato',
|
||||
namespaceId: "ID Namespace",
|
||||
namespaceIdPlaceholder: "Lascia vuoto per generazione automatica Nacos",
|
||||
namespaceIdPlaceholder: "Lascia vuoto per far generare a DBX un UUID",
|
||||
namespaceName: "Nome Namespace",
|
||||
namespaceNamePlaceholder: "Inserisci un nome namespace",
|
||||
namespaceDesc: "Descrizione",
|
||||
@@ -6809,10 +6809,18 @@ export default withEnglishFallback({
|
||||
readOnly: "Sola lettura",
|
||||
nacosConnectionPlan: "Piano di connessione",
|
||||
nacosConnectionPlanDescription: "Scegli l'implementazione del servizio e la versione API.",
|
||||
nacosApiPlane: "Modalità API Nacos 3",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "Consigliata · gestione completa",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "Limitata · per accesso alla sola console",
|
||||
nacosServiceAddress: "Indirizzo del servizio",
|
||||
nacosServiceAddressHint: "Inserisci l'indirizzo del servizio. La porta predefinita è 8848, ad esempio http://host:8848/nacos.",
|
||||
nacosConsoleServiceAddressHint: "Inserisci l'indirizzo della Console API. La porta predefinita è 8080, ad esempio http://host:8080.",
|
||||
nacosManagedNamespaces: "ID namespace gestiti",
|
||||
nacosManagedNamespacesNameOrId: "Nomi o ID dei namespace gestiti",
|
||||
nacosManagedNamespacesPlaceholder: "Ad esempio: public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "Ad esempio: public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "Inserire gli ID accessibili a questo utente normale, separati da virgole o righe. Il test di connessione verifica la lettura di configurazioni e servizi per ogni ID.",
|
||||
nacosOrdinaryAccount: "Utente normale",
|
||||
nacosOrdinaryAccountHint: "Se l'account sopra è un utente normale, attivare questa opzione e selezionare i namespace autorizzati; altrimenti la connessione non funzionerà correttamente.",
|
||||
@@ -6820,6 +6828,16 @@ export default withEnglishFallback({
|
||||
nacosOrdinaryNamespacesRequired: "Inserire almeno un ID namespace che questo utente normale può gestire.",
|
||||
nacosManagedNamespacesRequired: "Questo utente normale Nacos non può elencare i namespace o i dati di autorizzazione. Inserire almeno un ID che è autorizzato a gestire e riprovare.",
|
||||
nacosManagedNamespaceAccessDenied: "Questi namespace non hanno superato il controllo di lettura di configurazioni o servizi: {detail}",
|
||||
nacosNamespaceAccessScope: "Ambito di accesso ai namespace",
|
||||
nacosNamespaceAccessScopeHint: "Rileva prima i namespace accessibili. Se non possono essere elencati, inserisci manualmente ID verificati.",
|
||||
nacosNamespaceAccessAutomatic: "Rileva automaticamente",
|
||||
nacosNamespaceAccessManual: "Inserisci manualmente",
|
||||
nacosDetectAccessibleNamespaces: "Rileva e seleziona namespace accessibili",
|
||||
nacosDetectAccessibleNamespacesHint: "DBX verifica la lettura di configurazioni e servizi prima di proporre i namespace per {name}.",
|
||||
nacosV3AdminNamespaceScopeHint: "Le API Admin di Nacos 3 richiedono normalmente un account amministratore. Per un account limitato usa il piano Console API.",
|
||||
nacosV3AdminManagedNamespacesHint: "Questa connessione usa l'API Admin di Nacos 3 (normalmente porta 8848). Un utente normale deve inserire gli ID dei namespace, non i nomi visualizzati. Copia l'ID dai dettagli del namespace o dalle autorizzazioni del ruolo in Nacos Console.",
|
||||
nacosV3ConsoleManagedNamespacesHint: "Questa connessione usa l'API Console di Nacos 3 (normalmente porta 8080). Inserisci un nome o ID namespace; DBX converte il nome nell'ID effettivo quando puo leggere la directory.",
|
||||
rnacosNamespaceAccessScopeHint: "r-nacos ricava l'accesso ai namespace dai privilegi dell'utente corrente quando il profilo console è disponibile.",
|
||||
nacosAuth: "Autenticazione",
|
||||
nacosAuthHint: "Queste credenziali vengono usate per i test di connessione e la gestione quotidiana.",
|
||||
nacosUsernamePassword: "Nome utente / password",
|
||||
@@ -6827,6 +6845,7 @@ export default withEnglishFallback({
|
||||
nacosAdvancedHintDescription: "Configura qui metriche Prometheus, dimensione pagina, verifica TLS ed estensioni r-nacos.",
|
||||
nacosGoAdvanced: "Vai ad Avanzate",
|
||||
nacosV3ConsolePortWarning: "8080 è la porta predefinita della Web Console Nacos 3. Per la gestione dei servizi usa l'indirizzo Server / Admin API sulla porta 8848, ad esempio http://127.0.0.1:8848/nacos.",
|
||||
nacosV3AdminPortWarning: "8848 è normalmente la porta Admin API. Per Console API usa l'indirizzo della console, normalmente http://127.0.0.1:8080.",
|
||||
nacosAdvancedTitle: "Opzioni di connessione avanzate Nacos",
|
||||
nacosAdvancedDescription: "Questi parametri hanno valori sicuri e di solito richiedono modifiche solo per distribuzioni personalizzate, metriche o estensioni r-nacos.",
|
||||
nacosPageSizeHint: "Usato per gli elenchi di configurazioni e servizi; da 1 a 500 elementi per pagina.",
|
||||
@@ -6911,6 +6930,7 @@ export default withEnglishFallback({
|
||||
restore: "Ripristina",
|
||||
registerInstance: "Registra istanza",
|
||||
deregister: "Rimuovi",
|
||||
nacosContextPathHint: "Se lasciato vuoto, viene utilizzato il valore predefinito della modalità di connessione corrente; se si utilizza esplicitamente il percorso radice del servizio, inserire /; in scenari di proxy inverso, è possibile inserire prefissi come /nacos.",
|
||||
},
|
||||
databaseBrowser: {
|
||||
search: "Cerca database...",
|
||||
|
||||
@@ -6854,17 +6854,25 @@ export default withEnglishFallback({
|
||||
namespaceCreated: "名前空間「{name}」を作成しました",
|
||||
namespaceUpdated: "名前空間「{name}」を更新しました",
|
||||
namespaceId: "名前空間ID",
|
||||
namespaceIdPlaceholder: "空欄の場合はNacosが生成",
|
||||
namespaceIdPlaceholder: "空欄の場合はDBXがUUIDを生成",
|
||||
namespaceName: "名前空間名",
|
||||
namespaceNamePlaceholder: "名前空間名を入力",
|
||||
namespaceDesc: "説明",
|
||||
namespaceDescPlaceholder: "デフォルトは名前空間名",
|
||||
nacosConnectionPlan: "接続方式",
|
||||
nacosConnectionPlanDescription: "サービス実装と API バージョンを選択します。",
|
||||
nacosApiPlane: "Nacos 3 API モード",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "推奨 · 完全な管理機能",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "制限付き · Console のみの環境向け",
|
||||
nacosServiceAddress: "サービスアドレス",
|
||||
nacosServiceAddressHint: "サービスアドレスを入力してください。既定のポートは 8848 です(例: http://host:8848/nacos)。",
|
||||
nacosConsoleServiceAddressHint: "Console API アドレスを入力してください。既定のポートは 8080 です(例: http://host:8080)。",
|
||||
nacosManagedNamespaces: "管理対象の名前空間 ID",
|
||||
nacosManagedNamespacesNameOrId: "管理対象の名前空間名または ID",
|
||||
nacosManagedNamespacesPlaceholder: "例: public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "例: public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "この一般ユーザーがアクセスできる名前空間 ID をカンマまたは改行区切りで入力してください。接続テストで各 ID の設定・サービス読み取り権限を確認します。",
|
||||
nacosOrdinaryAccount: "一般ユーザー",
|
||||
nacosOrdinaryAccountHint: "上記のアカウントが一般ユーザーの場合、この項目を有効にしてアクセス権のある名前空間を指定してください。指定しないと接続後に正常に使用できません。",
|
||||
@@ -6872,6 +6880,16 @@ export default withEnglishFallback({
|
||||
nacosOrdinaryNamespacesRequired: "一般ユーザーが管理を許可された名前空間 ID を 1 つ以上入力してください。",
|
||||
nacosManagedNamespacesRequired: "この Nacos 一般ユーザーは名前空間または権限情報を一覧表示できません。管理を許可された名前空間 ID を 1 つ以上入力して再試行してください。",
|
||||
nacosManagedNamespaceAccessDenied: "次の名前空間は設定またはサービスの読み取り権限チェックに失敗しました: {detail}",
|
||||
nacosNamespaceAccessScope: "名前空間のアクセス範囲",
|
||||
nacosNamespaceAccessScopeHint: "まずアクセス可能な名前空間を検出してください。一覧取得できない場合は、確認済みの ID を手動で入力できます。",
|
||||
nacosNamespaceAccessAutomatic: "自動検出",
|
||||
nacosNamespaceAccessManual: "手動入力",
|
||||
nacosDetectAccessibleNamespaces: "アクセス可能な名前空間を検出して選択",
|
||||
nacosDetectAccessibleNamespacesHint: "DBX は {name} の名前空間を提示する前に、設定とサービスの読み取りアクセスを検証します。",
|
||||
nacosV3AdminNamespaceScopeHint: "Nacos 3 の Admin API には通常管理者アカウントが必要です。制限付きアカウントでは Console API を使用してください。",
|
||||
nacosV3AdminManagedNamespacesHint: "この接続は Nacos 3 Admin API(通常はポート 8848)を使用します。一般ユーザーは表示名ではなく名前空間 ID を入力する必要があります。Nacos Console の名前空間詳細またはロール権限リソースから ID をコピーしてください。",
|
||||
nacosV3ConsoleManagedNamespacesHint: "この接続は Nacos 3 Console API(通常はポート 8080)を使用します。名前空間名または ID を入力できます。ディレクトリを読み取れる場合、DBX は名前を実際の ID に変換します。",
|
||||
rnacosNamespaceAccessScopeHint: "r-nacos は Console プロファイルが利用可能な場合、現在のユーザーの名前空間権限からアクセス範囲を取得します。",
|
||||
nacosAuth: "認証",
|
||||
nacosAuthHint: "接続テストと日常の管理に使用する認証情報です。",
|
||||
nacosUsernamePassword: "ユーザー名 / パスワード",
|
||||
@@ -6879,6 +6897,7 @@ export default withEnglishFallback({
|
||||
nacosAdvancedHintDescription: "Prometheus メトリクス、ページサイズ、TLS 検証、r-nacos 拡張を設定できます。",
|
||||
nacosGoAdvanced: "詳細へ移動",
|
||||
nacosV3ConsolePortWarning: "8080 は Nacos 3 Web Console の既定ポートです。サービス管理には 8848 の Server / Admin API アドレス(例: http://127.0.0.1:8848/nacos)を使用してください。",
|
||||
nacosV3AdminPortWarning: "8848 は通常 Admin API ポートです。Console API モードでは、通常 http://127.0.0.1:8080 の Console アドレスを使用してください。",
|
||||
nacosAdvancedTitle: "Nacos 詳細接続オプション",
|
||||
nacosAdvancedDescription: "安全な既定値が設定されています。通常はカスタム構成、メトリクス、r-nacos 拡張を使用する場合だけ変更します。",
|
||||
nacosPageSizeHint: "設定とサービスの一覧に使用します。1 ページあたり 1~500 件です。",
|
||||
@@ -6963,6 +6982,7 @@ export default withEnglishFallback({
|
||||
restore: "元に戻す",
|
||||
registerInstance: "インスタンスを登録",
|
||||
deregister: "解除",
|
||||
nacosContextPathHint: "空欄の場合は現在の接続モードのデフォルト値を使用します。サービスルートパスを明示的に使用する場合は / を入力し、リバースプロキシのシナリオでは /nacos などのプレフィックスを入力できます。",
|
||||
},
|
||||
databaseBrowser: {
|
||||
search: "データベースを検索...",
|
||||
|
||||
@@ -7335,12 +7335,16 @@ export default withEnglishFallback({
|
||||
namespace: "네임스페이스",
|
||||
createNamespace: "네임스페이스 만들기",
|
||||
editNamespace: "네임스페이스 편집",
|
||||
deleteNamespace: "네임스페이스 삭제",
|
||||
deleteNamespaceDescription: "네임스페이스 {name}({id})을 삭제하시겠습니까? 구성과 서비스에 더 이상 접근하지 못할 수 있으며 이 작업은 되돌릴 수 없습니다.",
|
||||
creatingNamespace: "만드는 중...",
|
||||
updatingNamespace: "업데이트 중...",
|
||||
deletingNamespace: "삭제 중...",
|
||||
namespaceCreated: '네임스페이스 "{name}" 생성됨',
|
||||
namespaceUpdated: '네임스페이스 "{name}" 업데이트됨',
|
||||
namespaceDeleted: '네임스페이스 "{name}" 삭제됨',
|
||||
namespaceId: "네임스페이스 ID",
|
||||
namespaceIdPlaceholder: "Nacos가 자동 생성하려면 비워 두세요",
|
||||
namespaceIdPlaceholder: "DBX가 UUID를 자동 생성하도록 비워 두세요",
|
||||
namespaceName: "네임스페이스 이름",
|
||||
namespaceNamePlaceholder: "네임스페이스 이름 입력",
|
||||
namespaceDesc: "설명",
|
||||
@@ -7571,10 +7575,18 @@ export default withEnglishFallback({
|
||||
readOnly: "읽기 전용",
|
||||
nacosConnectionPlan: "연결 방식",
|
||||
nacosConnectionPlanDescription: "서비스 구현과 API 버전을 선택합니다.",
|
||||
nacosApiPlane: "Nacos 3 API 모드",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "권장 · 전체 관리 기능",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "제한됨 · Console 전용 환경용",
|
||||
nacosServiceAddress: "서비스 주소",
|
||||
nacosServiceAddressHint: "서비스 주소를 입력하세요. 기본 포트는 8848입니다(예: http://host:8848/nacos).",
|
||||
nacosConsoleServiceAddressHint: "Console API 주소를 입력하세요. 기본 포트는 8080입니다(예: http://host:8080).",
|
||||
nacosManagedNamespaces: "관리할 네임스페이스 ID",
|
||||
nacosManagedNamespacesNameOrId: "관리할 네임스페이스 이름 또는 ID",
|
||||
nacosManagedNamespacesPlaceholder: "예: public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "예: public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "이 일반 사용자가 접근할 수 있는 네임스페이스 ID를 쉼표 또는 줄바꿈으로 입력하세요. 연결 테스트에서 각 ID의 구성 및 서비스 읽기 권한을 확인합니다.",
|
||||
nacosOrdinaryAccount: "일반 사용자",
|
||||
nacosOrdinaryAccountHint: "위 계정이 일반 사용자인 경우 이 옵션을 켜고 권한이 있는 네임스페이스를 지정하세요. 지정하지 않으면 연결 후 정상적으로 사용할 수 없습니다.",
|
||||
@@ -7582,6 +7594,16 @@ export default withEnglishFallback({
|
||||
nacosOrdinaryNamespacesRequired: "일반 사용자가 관리할 수 있는 네임스페이스 ID를 하나 이상 입력하세요.",
|
||||
nacosManagedNamespacesRequired: "이 Nacos 일반 사용자는 네임스페이스 또는 권한 정보를 조회할 수 없습니다. 관리 권한이 있는 네임스페이스 ID를 하나 이상 입력한 후 다시 시도하세요.",
|
||||
nacosManagedNamespaceAccessDenied: "다음 네임스페이스가 구성 또는 서비스 읽기 권한 검사에 실패했습니다: {detail}",
|
||||
nacosNamespaceAccessScope: "네임스페이스 접근 범위",
|
||||
nacosNamespaceAccessScopeHint: "먼저 접근 가능한 네임스페이스를 감지하세요. 나열할 수 없으면 확인된 ID를 직접 입력할 수 있습니다.",
|
||||
nacosNamespaceAccessAutomatic: "자동 감지",
|
||||
nacosNamespaceAccessManual: "수동 입력",
|
||||
nacosDetectAccessibleNamespaces: "접근 가능한 네임스페이스 감지 및 선택",
|
||||
nacosDetectAccessibleNamespacesHint: "DBX는 {name}에 대해 네임스페이스를 표시하기 전에 구성 및 서비스 읽기 권한을 확인합니다.",
|
||||
nacosV3AdminNamespaceScopeHint: "Nacos 3 Admin API는 일반적으로 관리자 계정이 필요합니다. 제한된 계정은 Console API를 사용하세요.",
|
||||
nacosV3AdminManagedNamespacesHint: "이 연결은 Nacos 3 Admin API(일반적으로 포트 8848)를 사용합니다. 일반 사용자는 표시 이름이 아니라 네임스페이스 ID를 입력해야 합니다. Nacos Console의 네임스페이스 상세 정보 또는 역할 권한 리소스에서 ID를 복사하세요.",
|
||||
nacosV3ConsoleManagedNamespacesHint: "이 연결은 Nacos 3 Console API(일반적으로 포트 8080)를 사용합니다. 네임스페이스 이름이나 ID를 입력할 수 있으며, 디렉터리를 읽을 수 있으면 DBX가 이름을 실제 ID로 변환합니다.",
|
||||
rnacosNamespaceAccessScopeHint: "r-nacos는 콘솔 프로필을 사용할 수 있을 때 현재 사용자의 네임스페이스 권한에서 접근 범위를 가져옵니다.",
|
||||
nacosAuth: "인증",
|
||||
nacosAuthHint: "이 자격 증명은 연결 테스트와 일상적인 관리에 사용됩니다.",
|
||||
nacosUsernamePassword: "사용자 이름 / 비밀번호",
|
||||
@@ -7589,8 +7611,10 @@ export default withEnglishFallback({
|
||||
nacosAdvancedHintDescription: "Prometheus 메트릭, 페이지 크기, TLS 검증 및 r-nacos 확장을 설정합니다.",
|
||||
nacosGoAdvanced: "고급으로 이동",
|
||||
nacosV3ConsolePortWarning: "8080은 Nacos 3 Web Console의 기본 포트입니다. 서비스 관리는 8848 포트의 Server / Admin API 주소(예: http://127.0.0.1:8848/nacos)를 사용하세요.",
|
||||
nacosV3AdminPortWarning: "8848은 일반적으로 Admin API 포트입니다. Console API 모드에서는 보통 http://127.0.0.1:8080인 Console 주소를 사용하세요.",
|
||||
nacosAdvancedTitle: "Nacos 고급 연결 옵션",
|
||||
nacosAdvancedDescription: "안전한 기본값이 적용되어 있으며, 사용자 지정 배포·메트릭·r-nacos 확장을 사용할 때만 조정하면 됩니다.",
|
||||
nacosContextPathHint: "선택한 프로필의 기본값을 사용하려면 비워 두세요. 서버 루트를 명시하려면 /를 입력하고, 리버스 프록시 접두사는 /nacos와 같이 입력하세요.",
|
||||
nacosPageSizeHint: "구성 및 서비스 목록에 사용되며 페이지당 1~500개입니다.",
|
||||
nacosMetricsHint: "서비스 주소에서 메트릭을 자동으로 조회합니다. 메트릭 엔드포인트가 다르면 사용자 지정 URL을 선택하세요.",
|
||||
nacosRnacosExtension: "r-nacos 확장",
|
||||
|
||||
@@ -25,6 +25,15 @@ export const nacosAccessControlMessages = {
|
||||
createUser: "Create user",
|
||||
editUser: "Edit user",
|
||||
resetUserPassword: "Reset password",
|
||||
passwordResetSucceeded: "Reset password for {username}",
|
||||
passwordResetFailed: "Failed to reset password: {message}",
|
||||
currentPasswordSaved: "The saved connection credential was updated and the connection will use the new password.",
|
||||
currentSessionPasswordUpdated: "The in-memory connection credential was updated for this session and was not saved to disk.",
|
||||
currentPasswordNotSaved: "This connection does not save passwords. Enter the new password when reconnecting.",
|
||||
deleteNamespace: "Delete namespace",
|
||||
deleteNamespaceDescription: "Delete namespace {name} ({id})? Its configurations and services may become inaccessible. This cannot be undone.",
|
||||
deletingNamespace: "Deleting...",
|
||||
namespaceDeleted: 'Namespace "{name}" deleted',
|
||||
deleteUser: "Delete user",
|
||||
noUsers: "No users found",
|
||||
noRoleBindings: "No role bindings found",
|
||||
@@ -78,7 +87,7 @@ export const nacosAccessControlMessages = {
|
||||
accessGlobalAdministrator: "Global administrator",
|
||||
accessNeedsRepair: "Needs repair",
|
||||
accessRoleSubtitle: "{members} associated members",
|
||||
accessIncompleteRoleHint: "This historical role has no members or namespace permissions. Add both before saving changes.",
|
||||
accessIncompleteRoleHint: "This role has no members or namespace permissions. Add namespace permissions before saving; members may remain empty.",
|
||||
accessPermissionScope: "Permission scope",
|
||||
accessPermissionScopeHint: "Namespace rules managed by this role.",
|
||||
accessAllResources: "All resources",
|
||||
@@ -107,19 +116,26 @@ export const nacosAccessControlMessages = {
|
||||
accessInlineUserRequired: "Every new user requires a username and password.",
|
||||
accessNoAssignableRoles: "No complete roles are available",
|
||||
accessTypeUsername: "Type {value} to confirm this high-risk account change",
|
||||
accessDeleteUserConfirmationLabel: "Enter the complete username to confirm deletion",
|
||||
accessDeleteUserConfirmationHint: "Enter this text exactly (case-sensitive):",
|
||||
accessDeleteUserConfirmationPlaceholder: "Complete username",
|
||||
accessMembersRequiredHint: "Select existing users or create users inline. At least one is required.",
|
||||
accessMembersOptionalHint: "An existing ordinary role may keep its permissions without any associated members.",
|
||||
accessInlineCreateUser: "Create user inline",
|
||||
accessAdminPermissionImmutable: "The global administrator permission is built into Nacos and cannot be edited as namespace rules.",
|
||||
accessPermissionRequiredHint: "Select namespaces, then set the action for each one.",
|
||||
accessUnassignedNamespaces: "Unassigned namespaces",
|
||||
accessGrantedNamespaces: "Granted namespaces",
|
||||
accessSearchNamespaces: "Search namespaces",
|
||||
accessSearchNamespaces: "Search by namespace name or ID",
|
||||
accessNoUnassignedNamespaces: "No namespaces available to add",
|
||||
accessNoGrantedNamespaces: "Select namespaces to grant access",
|
||||
accessGrantNamespaces: "Grant selected namespaces",
|
||||
accessRevokeNamespaces: "Remove selected namespaces",
|
||||
accessDeleteRoleHint: "Deleting {role} removes every permission and member binding. Successful deletions are not restored after a partial failure.",
|
||||
accessTypeRole: "Type {value} to confirm role deletion",
|
||||
accessDeleteRoleConfirmationLabel: "Enter the complete role name to confirm deletion",
|
||||
accessDeleteRoleConfirmationHint: "Enter this text exactly (case-sensitive):",
|
||||
accessDeleteRoleConfirmationPlaceholder: "Complete role name",
|
||||
accessOperationResult: "Operation result",
|
||||
accessReenterPassword: "Enter the password again to retry",
|
||||
accessUndoCreated: "Undo created items",
|
||||
|
||||
@@ -6579,7 +6579,7 @@ export default withEnglishFallback({
|
||||
namespaceCreated: 'Namespace "{name}" criado',
|
||||
namespaceUpdated: 'Namespace "{name}" atualizado',
|
||||
namespaceId: "ID do Namespace",
|
||||
namespaceIdPlaceholder: "Deixe vazio para o Nacos gerar",
|
||||
namespaceIdPlaceholder: "Deixe vazio para o DBX gerar um UUID",
|
||||
namespaceName: "Nome do Namespace",
|
||||
namespaceNamePlaceholder: "Digite um nome de namespace",
|
||||
namespaceDesc: "Descrição",
|
||||
@@ -6810,10 +6810,18 @@ export default withEnglishFallback({
|
||||
readOnly: "Somente leitura",
|
||||
nacosConnectionPlan: "Plano de conexão",
|
||||
nacosConnectionPlanDescription: "Escolha a implementação do serviço e a versão da API.",
|
||||
nacosApiPlane: "Modo de API do Nacos 3",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "Recomendado · gerenciamento completo",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "Limitado · para acesso somente ao console",
|
||||
nacosServiceAddress: "Endereço do serviço",
|
||||
nacosServiceAddressHint: "Informe o endereço do serviço. A porta padrão é 8848, por exemplo http://host:8848/nacos.",
|
||||
nacosConsoleServiceAddressHint: "Informe o endereço da Console API. A porta padrão é 8080, por exemplo http://host:8080.",
|
||||
nacosManagedNamespaces: "IDs de namespaces gerenciados",
|
||||
nacosManagedNamespacesNameOrId: "Nomes ou IDs de namespaces gerenciados",
|
||||
nacosManagedNamespacesPlaceholder: "Por exemplo: public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "Por exemplo: public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "Informe os IDs acessíveis para este usuário comum, separados por vírgulas ou linhas. O teste de conexão valida a leitura de configurações e serviços para cada ID.",
|
||||
nacosOrdinaryAccount: "Usuário comum",
|
||||
nacosOrdinaryAccountHint: "Se a conta acima for um usuário comum, ative esta opção e selecione os namespaces autorizados; caso contrário, a conexão não funcionará corretamente.",
|
||||
@@ -6821,6 +6829,16 @@ export default withEnglishFallback({
|
||||
nacosOrdinaryNamespacesRequired: "Informe pelo menos um ID de namespace que este usuário comum pode gerenciar.",
|
||||
nacosManagedNamespacesRequired: "Este usuário comum do Nacos não pode listar namespaces nem dados de autorização. Informe pelo menos um ID que ele tenha permissão para gerenciar e tente novamente.",
|
||||
nacosManagedNamespaceAccessDenied: "Estes namespaces falharam na verificação de leitura de configurações ou serviços: {detail}",
|
||||
nacosNamespaceAccessScope: "Escopo de acesso aos namespaces",
|
||||
nacosNamespaceAccessScopeHint: "Primeiro detecte os namespaces acessíveis. Se não puder listá-los, informe manualmente IDs verificados.",
|
||||
nacosNamespaceAccessAutomatic: "Detectar automaticamente",
|
||||
nacosNamespaceAccessManual: "Informar manualmente",
|
||||
nacosDetectAccessibleNamespaces: "Detectar e selecionar namespaces acessíveis",
|
||||
nacosDetectAccessibleNamespacesHint: "O DBX verifica a leitura de configurações e serviços antes de oferecer namespaces para {name}.",
|
||||
nacosV3AdminNamespaceScopeHint: "As APIs Admin do Nacos 3 normalmente exigem uma conta de administrador. Para uma conta restrita, use o plano Console API.",
|
||||
nacosV3AdminManagedNamespacesHint: "Esta conexao usa a API Admin do Nacos 3 (normalmente a porta 8848). Um usuario comum deve informar IDs de namespace, nao nomes exibidos. Copie o ID dos detalhes do namespace ou das permissoes da funcao no Nacos Console.",
|
||||
nacosV3ConsoleManagedNamespacesHint: "Esta conexao usa a API Console do Nacos 3 (normalmente a porta 8080). Informe um nome ou ID de namespace; o DBX converte o nome no ID real quando consegue ler o diretorio.",
|
||||
rnacosNamespaceAccessScopeHint: "O r-nacos deriva o acesso aos namespaces dos privilégios do usuário atual quando o perfil do console está disponível.",
|
||||
nacosAuth: "Autenticação",
|
||||
nacosAuthHint: "Essas credenciais são usadas nos testes de conexão e no gerenciamento diário.",
|
||||
nacosUsernamePassword: "Usuário / senha",
|
||||
@@ -6828,6 +6846,7 @@ export default withEnglishFallback({
|
||||
nacosAdvancedHintDescription: "Configure ali as métricas do Prometheus, o tamanho da página, a validação TLS e as extensões do r-nacos.",
|
||||
nacosGoAdvanced: "Ir para Avançado",
|
||||
nacosV3ConsolePortWarning: "8080 é a porta padrão do Web Console do Nacos 3. Para gerenciar serviços, use o endereço Server / Admin API na porta 8848, por exemplo http://127.0.0.1:8848/nacos.",
|
||||
nacosV3AdminPortWarning: "8848 normalmente é a porta Admin API. No modo Console API, use o endereço do console, normalmente http://127.0.0.1:8080.",
|
||||
nacosAdvancedTitle: "Opções avançadas de conexão do Nacos",
|
||||
nacosAdvancedDescription: "Esses parâmetros têm valores seguros e normalmente só precisam ser ajustados para implantações personalizadas, métricas ou extensões do r-nacos.",
|
||||
nacosPageSizeHint: "Usado nas listas de configurações e serviços; de 1 a 500 itens por página.",
|
||||
@@ -6912,6 +6931,7 @@ export default withEnglishFallback({
|
||||
restore: "Restaurar",
|
||||
registerInstance: "Registrar instância",
|
||||
deregister: "Remover registro",
|
||||
nacosContextPathHint: "Quando deixado em branco, use o valor padrão do modo de conexão atual; se usar explicitamente o caminho raiz do serviço, preencha /; em cenários de proxy reverso, você pode preencher prefixos como /nacos.",
|
||||
},
|
||||
databaseBrowser: {
|
||||
search: "Pesquisar bancos de dados...",
|
||||
|
||||
@@ -7944,6 +7944,11 @@ export default withEnglishFallback({
|
||||
createUser: "新建用户",
|
||||
editUser: "编辑用户",
|
||||
resetUserPassword: "重置密码",
|
||||
passwordResetSucceeded: "已重置 {username} 的密码",
|
||||
passwordResetFailed: "重置密码失败:{message}",
|
||||
currentPasswordSaved: "已同步更新当前连接保存的凭据,后续将使用新密码连接。",
|
||||
currentSessionPasswordUpdated: "已更新本次运行期的内存凭据,后续连接将使用新密码,且不会写入磁盘。",
|
||||
currentPasswordNotSaved: "当前连接未保存密码,重新连接时需要输入新密码。",
|
||||
deleteUser: "删除用户",
|
||||
noUsers: "暂无用户",
|
||||
noRoleBindings: "暂无角色绑定",
|
||||
@@ -7997,7 +8002,7 @@ export default withEnglishFallback({
|
||||
accessGlobalAdministrator: "全局管理员",
|
||||
accessNeedsRepair: "需要修复",
|
||||
accessRoleSubtitle: "已关联 {members} 位成员",
|
||||
accessIncompleteRoleHint: "这是一个历史异常角色,缺少成员或命名空间权限。保存修改前必须补齐两项配置。",
|
||||
accessIncompleteRoleHint: "该角色当前没有成员或命名空间权限。保存前需补充命名空间权限,成员可以保持为空。",
|
||||
accessPermissionScope: "权限范围",
|
||||
accessPermissionScopeHint: "由该角色管理的命名空间权限规则。",
|
||||
accessAllResources: "全部资源",
|
||||
@@ -8026,19 +8031,26 @@ export default withEnglishFallback({
|
||||
accessInlineUserRequired: "每个待创建用户都必须填写用户名和密码。",
|
||||
accessNoAssignableRoles: "暂无可分配的完整角色",
|
||||
accessTypeUsername: "输入 {value} 确认此次高风险账号变更",
|
||||
accessDeleteUserConfirmationLabel: "输入完整用户名以确认删除",
|
||||
accessDeleteUserConfirmationHint: "请准确输入以下内容(区分大小写):",
|
||||
accessDeleteUserConfirmationPlaceholder: "请输入完整用户名",
|
||||
accessMembersRequiredHint: "选择已有用户或直接创建新用户,至少需要一位成员。",
|
||||
accessMembersOptionalHint: "已有普通角色可以在没有关联成员时继续保留其权限。",
|
||||
accessInlineCreateUser: "同时创建用户",
|
||||
accessAdminPermissionImmutable: "全局管理员权限由 Nacos 内置,不能按普通命名空间权限编辑。",
|
||||
accessPermissionRequiredHint: "选择命名空间后,为每一项分别设置权限动作。",
|
||||
accessUnassignedNamespaces: "未授权命名空间",
|
||||
accessGrantedNamespaces: "已授权命名空间",
|
||||
accessSearchNamespaces: "搜索命名空间",
|
||||
accessSearchNamespaces: "按命名空间名称或 ID 搜索",
|
||||
accessNoUnassignedNamespaces: "没有可添加的命名空间",
|
||||
accessNoGrantedNamespaces: "请选择需要授权的命名空间",
|
||||
accessGrantNamespaces: "授予选中的命名空间",
|
||||
accessRevokeNamespaces: "移除选中的命名空间",
|
||||
accessDeleteRoleHint: "删除 {role} 会移除其全部权限和成员绑定。部分成功后,已删除内容不会自动恢复。",
|
||||
accessTypeRole: "输入 {value} 确认删除角色",
|
||||
accessDeleteRoleConfirmationLabel: "输入完整角色名称以确认删除",
|
||||
accessDeleteRoleConfirmationHint: "请准确输入以下内容(区分大小写):",
|
||||
accessDeleteRoleConfirmationPlaceholder: "请输入完整角色名称",
|
||||
accessOperationResult: "操作结果",
|
||||
accessReenterPassword: "重新输入密码后重试",
|
||||
accessUndoCreated: "撤销本次创建",
|
||||
@@ -8082,12 +8094,16 @@ export default withEnglishFallback({
|
||||
namespace: "命名空间",
|
||||
createNamespace: "新建命名空间",
|
||||
editNamespace: "编辑命名空间",
|
||||
deleteNamespace: "删除命名空间",
|
||||
deleteNamespaceDescription: "确定删除命名空间 {name}({id})吗?其中的配置和服务可能将无法访问,此操作不可撤销。",
|
||||
creatingNamespace: "正在创建...",
|
||||
updatingNamespace: "正在更新...",
|
||||
deletingNamespace: "正在删除...",
|
||||
namespaceCreated: "命名空间「{name}」已创建",
|
||||
namespaceUpdated: "命名空间「{name}」已更新",
|
||||
namespaceDeleted: "命名空间「{name}」已删除",
|
||||
namespaceId: "命名空间 ID",
|
||||
namespaceIdPlaceholder: "留空则由 Nacos 自动生成",
|
||||
namespaceIdPlaceholder: "留空则由 DBX 自动生成 UUID",
|
||||
namespaceName: "命名空间名",
|
||||
namespaceNamePlaceholder: "请输入命名空间名",
|
||||
namespaceDesc: "描述",
|
||||
@@ -8318,10 +8334,18 @@ export default withEnglishFallback({
|
||||
readOnly: "只读",
|
||||
nacosConnectionPlan: "连接方案",
|
||||
nacosConnectionPlanDescription: "选择服务实现和 API 版本。",
|
||||
nacosApiPlane: "Nacos 3 接口模式",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "推荐 · 完整管理能力",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "受限 · 适用于仅开放控制台的环境",
|
||||
nacosServiceAddress: "服务地址",
|
||||
nacosServiceAddressHint: "填写服务地址即可,默认端口为 8848,例如 http://主机:8848/nacos。",
|
||||
nacosConsoleServiceAddressHint: "填写 Console API 地址,默认端口为 8080,例如 http://主机:8080。",
|
||||
nacosManagedNamespaces: "可管理命名空间 ID",
|
||||
nacosManagedNamespacesNameOrId: "可管理命名空间名称或 ID",
|
||||
nacosManagedNamespacesPlaceholder: "例如:public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "例如:public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "填写该普通用户有权访问的命名空间 ID,支持逗号或换行分隔;连接测试会逐项验证配置与服务读取权限。",
|
||||
nacosOrdinaryAccount: "普通用户",
|
||||
nacosOrdinaryAccountHint: "如果上方填写的是普通用户,请开启此选项并指定该用户有权限的命名空间,否则连接后将无法正常使用。",
|
||||
@@ -8329,6 +8353,16 @@ export default withEnglishFallback({
|
||||
nacosOrdinaryNamespacesRequired: "普通用户至少需要填写一个有权管理的命名空间 ID。",
|
||||
nacosManagedNamespacesRequired: "当前 Nacos 普通用户无权获取命名空间目录或权限管理信息,请填写至少一个有权管理的命名空间 ID 后重试。",
|
||||
nacosManagedNamespaceAccessDenied: "以下命名空间未通过配置或服务读取权限校验:{detail}",
|
||||
nacosNamespaceAccessScope: "命名空间访问范围",
|
||||
nacosNamespaceAccessScopeHint: "请先检测可访问命名空间;无法枚举时,可手动填写已确认有权限的命名空间 ID。",
|
||||
nacosNamespaceAccessAutomatic: "自动检测",
|
||||
nacosNamespaceAccessManual: "手动填写",
|
||||
nacosDetectAccessibleNamespaces: "检测并选择可访问命名空间",
|
||||
nacosDetectAccessibleNamespacesHint: "DBX 会校验「{name}」中每个命名空间的配置与服务读取权限后再供选择。",
|
||||
nacosV3AdminNamespaceScopeHint: "Nacos 3 Admin API 通常需要管理员账号。受限账号请改用 Console API 平面,并在那里配置命名空间访问范围。",
|
||||
nacosV3AdminManagedNamespacesHint: "当前使用 Nacos 3 Admin API(通常为 8848)。普通用户必须填写命名空间 ID,不能填写显示名称。请从 Nacos Console 的命名空间详情或角色权限资源中复制 ID。",
|
||||
nacosV3ConsoleManagedNamespacesHint: "当前使用 Nacos 3 Console API(通常为 8080)。可填写命名空间名称或 ID;DBX 能读取目录时会自动将名称转换为实际 ID。",
|
||||
rnacosNamespaceAccessScopeHint: "配置了 r-nacos Console 用户资料后,DBX 会根据当前用户的命名空间权限确定访问范围。",
|
||||
nacosAuth: "认证",
|
||||
nacosAuthHint: "认证信息同时用于连接测试和日常管理。",
|
||||
nacosUsernamePassword: "账号密码",
|
||||
@@ -8336,8 +8370,10 @@ export default withEnglishFallback({
|
||||
nacosAdvancedHintDescription: "可在高级页配置 Prometheus 指标、分页大小、TLS 证书校验和 r-nacos 扩展。",
|
||||
nacosGoAdvanced: "前往高级",
|
||||
nacosV3ConsolePortWarning: "8080 是 Nacos 3 默认 Web 控制台端口。服务管理请使用 8848 端口的 Server / Admin API 地址,例如 http://127.0.0.1:8848/nacos。",
|
||||
nacosV3AdminPortWarning: "8848 通常是 Admin API 端口。Console API 模式请填写控制台地址,默认是 http://127.0.0.1:8080。",
|
||||
nacosAdvancedTitle: "Nacos 高级连接选项",
|
||||
nacosAdvancedDescription: "这些参数均有安全默认值,通常只有自定义部署、指标采集或 r-nacos 扩展能力需要调整。",
|
||||
nacosContextPathHint: "留空时使用当前连接模式的默认值;明确使用服务根路径请填写 /,反向代理场景可填写 /nacos 等前缀。",
|
||||
nacosPageSizeHint: "用于配置和服务列表,每页 1–500 条。",
|
||||
nacosMetricsHint: "自动使用服务地址查询指标;若指标端点不同,请选择“自定义地址”。",
|
||||
nacosRnacosExtension: "r-nacos 控制台功能",
|
||||
|
||||
@@ -6126,17 +6126,25 @@ export default withEnglishFallback({
|
||||
namespaceCreated: "命名空間「{name}」已建立",
|
||||
namespaceUpdated: "命名空間「{name}」已更新",
|
||||
namespaceId: "命名空間 ID",
|
||||
namespaceIdPlaceholder: "留空由 Nacos 自動產生",
|
||||
namespaceIdPlaceholder: "留空則由 DBX 自動產生 UUID",
|
||||
namespaceName: "命名空間名稱",
|
||||
namespaceNamePlaceholder: "輸入命名空間名稱",
|
||||
namespaceDesc: "描述",
|
||||
namespaceDescPlaceholder: "預設為命名空間名稱",
|
||||
nacosConnectionPlan: "連線方案",
|
||||
nacosConnectionPlanDescription: "選擇服務實作與 API 版本。",
|
||||
nacosApiPlane: "Nacos 3 API 模式",
|
||||
nacosApiPlaneAdmin: "Admin API",
|
||||
nacosApiPlaneAdminHint: "建議 · 完整管理能力",
|
||||
nacosApiPlaneConsole: "Console API",
|
||||
nacosApiPlaneConsoleHint: "受限 · 適用於僅開放控制台的環境",
|
||||
nacosServiceAddress: "服務位址",
|
||||
nacosServiceAddressHint: "填寫服務位址即可,預設連接埠為 8848,例如 http://host:8848/nacos。",
|
||||
nacosConsoleServiceAddressHint: "填寫 Console API 位址,預設連接埠為 8080,例如 http://host:8080。",
|
||||
nacosManagedNamespaces: "可管理命名空間 ID",
|
||||
nacosManagedNamespacesNameOrId: "可管理命名空間名稱或 ID",
|
||||
nacosManagedNamespacesPlaceholder: "例如:public, team-a",
|
||||
nacosManagedNamespacesIdPlaceholder: "例如:public, f8b8c4e2-8195-4041-adb2-683a8259384b",
|
||||
nacosManagedNamespacesHint: "填寫該一般使用者有權存取的命名空間 ID,支援逗號或換行分隔;連線測試會逐項驗證設定與服務讀取權限。",
|
||||
nacosOrdinaryAccount: "一般使用者",
|
||||
nacosOrdinaryAccountHint: "如果上方填寫的是一般使用者,請開啟此選項並指定該使用者有權限的命名空間,否則連線後將無法正常使用。",
|
||||
@@ -6144,6 +6152,16 @@ export default withEnglishFallback({
|
||||
nacosOrdinaryNamespacesRequired: "一般使用者至少需要填寫一個有權管理的命名空間 ID。",
|
||||
nacosManagedNamespacesRequired: "目前的 Nacos 一般使用者無權取得命名空間目錄或權限管理資訊,請填寫至少一個有權管理的命名空間 ID 後重試。",
|
||||
nacosManagedNamespaceAccessDenied: "以下命名空間未通過設定或服務讀取權限驗證:{detail}",
|
||||
nacosNamespaceAccessScope: "命名空間存取範圍",
|
||||
nacosNamespaceAccessScopeHint: "請先偵測可存取的命名空間;無法列舉時,可手動填寫已確認有權限的命名空間 ID。",
|
||||
nacosNamespaceAccessAutomatic: "自動偵測",
|
||||
nacosNamespaceAccessManual: "手動填寫",
|
||||
nacosDetectAccessibleNamespaces: "偵測並選擇可存取的命名空間",
|
||||
nacosDetectAccessibleNamespacesHint: "DBX 會驗證「{name}」中每個命名空間的設定與服務讀取權限後再供選擇。",
|
||||
nacosV3AdminNamespaceScopeHint: "Nacos 3 Admin API 通常需要管理員帳號。受限帳號請改用 Console API 平面,並在其中設定命名空間存取範圍。",
|
||||
nacosV3AdminManagedNamespacesHint: "目前使用 Nacos 3 Admin API(通常為 8848)。一般使用者必須填寫命名空間 ID,而非顯示名稱。請從 Nacos Console 的命名空間詳細資料或角色權限資源複製 ID。",
|
||||
nacosV3ConsoleManagedNamespacesHint: "目前使用 Nacos 3 Console API(通常為 8080)。可填寫命名空間名稱或 ID;DBX 能讀取目錄時會自動將名稱轉換為實際 ID。",
|
||||
rnacosNamespaceAccessScopeHint: "設定 r-nacos Console 使用者資料後,DBX 會依目前使用者的命名空間權限決定存取範圍。",
|
||||
nacosAuth: "驗證",
|
||||
nacosAuthHint: "這些驗證資訊會用於連線測試與日常管理。",
|
||||
nacosUsernamePassword: "使用者名稱 / 密碼",
|
||||
@@ -6151,6 +6169,7 @@ export default withEnglishFallback({
|
||||
nacosAdvancedHintDescription: "可在此設定 Prometheus 指標、分頁大小、TLS 驗證與 r-nacos 擴充功能。",
|
||||
nacosGoAdvanced: "前往進階",
|
||||
nacosV3ConsolePortWarning: "8080 是 Nacos 3 Web Console 的預設連接埠。服務管理請使用 8848 連接埠的 Server / Admin API 位址,例如 http://127.0.0.1:8848/nacos。",
|
||||
nacosV3AdminPortWarning: "8848 通常是 Admin API 連接埠。Console API 模式請填寫控制台位址,預設為 http://127.0.0.1:8080。",
|
||||
nacosAdvancedTitle: "Nacos 進階連線選項",
|
||||
nacosAdvancedDescription: "這些參數都有安全的預設值,通常只有自訂部署、指標或 r-nacos 擴充功能需要調整。",
|
||||
nacosPageSizeHint: "用於設定與服務清單,每頁 1–500 筆。",
|
||||
@@ -6235,6 +6254,7 @@ export default withEnglishFallback({
|
||||
restore: "還原",
|
||||
registerInstance: "註冊實例",
|
||||
deregister: "註銷",
|
||||
nacosContextPathHint: "留空時使用目前連線模式的預設值;明確使用服務根路徑請填寫 /,反向代理情境可填寫 /nacos 等前綴。",
|
||||
},
|
||||
userAdmin: {
|
||||
title: "使用者與權限",
|
||||
|
||||
@@ -13,6 +13,13 @@ describe("Nacos connection dialog layout", () => {
|
||||
expect(source).not.toContain("dockerNacosConsoleFallbackUrl");
|
||||
});
|
||||
|
||||
it("persists the explicit Nacos 3 Admin or Console API selection", () => {
|
||||
expect(source).toContain('const nacosApiPlane = ref<NacosApiPlane>("admin")');
|
||||
expect(source).toContain('config?.apiPlane || "admin"');
|
||||
expect(source).toContain('apiPlane: nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" ? nacosApiPlane.value : undefined');
|
||||
expect(source).toContain("nacos.nacosConsoleServiceAddressHint");
|
||||
});
|
||||
|
||||
it("keeps the primary form focused on endpoints and authentication", () => {
|
||||
const mainStart = source.indexOf("data-nacos-profile-selector");
|
||||
const mainEnd = source.indexOf("<!-- Redis: host, port, user, password, ssl -->", mainStart);
|
||||
@@ -24,13 +31,11 @@ describe("Nacos connection dialog layout", () => {
|
||||
expect(main).toContain('t("nacos.nacosAdvancedHint")');
|
||||
expect(main).toContain("@click=\"configTab = 'advanced'\"");
|
||||
expect(main).toContain('v-model="nacosServerAddr"');
|
||||
expect(main).toContain('v-model="nacosManagedNamespacesText"');
|
||||
expect(main).toContain('t("nacos.nacosManagedNamespacesHint")');
|
||||
expect(main).toContain("data-nacos-ordinary-user-toggle");
|
||||
expect(main).toContain('v-model="nacosOrdinaryAccount"');
|
||||
expect(main).not.toContain('v-model="nacosManagedNamespacesText"');
|
||||
expect(main).not.toContain("data-nacos-namespace-access-scope");
|
||||
expect(main).not.toContain("data-nacos-ordinary-user-toggle");
|
||||
expect(main).not.toContain("nacosOrdinaryAccount");
|
||||
expect(main).toContain("nacosAuthKind === 'usernamePassword'");
|
||||
expect(main).toContain("nacosImplementation === 'nacos' && nacosAuthKind === 'usernamePassword'");
|
||||
expect(main).not.toContain("nacosVersionMode === 'v3' && nacosAuthKind === 'usernamePassword'");
|
||||
expect(main).not.toContain('v-model="nacosNamespace"');
|
||||
expect(main).toContain('t("nacos.nacosAuthHint")');
|
||||
expect(main).not.toContain('v-model="nacosMetricsMode"');
|
||||
@@ -44,6 +49,8 @@ describe("Nacos connection dialog layout", () => {
|
||||
|
||||
expect(advanced).not.toContain('v-model="nacosContextPathInput"');
|
||||
expect(advanced).not.toContain("配置上下文路径");
|
||||
expect(advanced).toContain('v-model="nacosContextPath"');
|
||||
expect(advanced).toContain('t("nacos.nacosContextPathHint")');
|
||||
expect(advanced).toContain('v-model="nacosMetricsMode"');
|
||||
expect(advanced).toContain('v-model="nacosRNacosConsoleAddr"');
|
||||
expect(advanced).toContain('v-if="nacosRNacosConsoleAddr.trim()"');
|
||||
@@ -58,17 +65,68 @@ describe("Nacos connection dialog layout", () => {
|
||||
expect(source).not.toContain("DBX 不需要配置该地址");
|
||||
const mainStart = source.indexOf("data-nacos-profile-selector");
|
||||
const mainEnd = source.indexOf("<!-- Redis: host, port, user, password, ssl -->", mainStart);
|
||||
expect(source.slice(mainStart, mainEnd)).toContain("data-nacos-managed-namespaces");
|
||||
expect(source.slice(mainStart, mainEnd)).not.toContain("data-nacos-managed-namespaces");
|
||||
expect(source.slice(mainStart, mainEnd)).not.toContain('placeholder="http://127.0.0.1:8080"');
|
||||
expect(source).not.toContain("http://127.0.0.1:8010");
|
||||
expect(source).not.toContain("http://127.0.0.1:8818");
|
||||
});
|
||||
|
||||
it("uses a dedicated namespace selector instead of the database selector", () => {
|
||||
expect(source).toContain('t("nacos.nacosVisibleNamespacesTitle")');
|
||||
expect(source).toContain("t(nacosNamespacePickerTitleKey)");
|
||||
expect(source).toContain('"nacos.nacosDetectAccessibleNamespaces"');
|
||||
expect(source).toContain("openVisibleNacosNamespacesPicker");
|
||||
expect(source).toContain("loadReadableNacosNamespaces(draftId, api)");
|
||||
expect(source).toContain("showVisibleNacosNamespacesDialog");
|
||||
expect(source).toContain('v-model="visibleNacosNamespaceAccessMode"');
|
||||
expect(source).toContain('TabsTrigger value="automatic"');
|
||||
expect(source).toContain('TabsTrigger v-if="canDetectNacosNamespaceAccess" value="manual"');
|
||||
expect(source).toContain('v-model="nacosManagedNamespacesText"');
|
||||
expect(source).not.toContain("!isNacosV3AdminPlane.value");
|
||||
expect(source).toContain('visibleNacosNamespaceAccessMode.value === "manual"');
|
||||
});
|
||||
|
||||
it("guides namespace listing permission failures to manual scope entry", () => {
|
||||
expect(source).toContain("function isNacosNamespaceListingPermissionError(error: unknown): boolean");
|
||||
expect(source).toContain("NACOS_ERROR\\[(?:v3ManagedNamespacesRequired|managedNamespacesRequired)\\]");
|
||||
expect(source).toContain("/\\/v3\\/(?:admin|console)\\/core\\/namespace\\/list/");
|
||||
expect(source).toContain("visibleNacosNamespaceListingPermissionDenied.value = isNacosNamespaceListingPermissionError(e)");
|
||||
expect(source).toContain('t("nacos.nacosManagedNamespacesRequired")');
|
||||
expect(source).toContain('v-else-if="visibleNacosNamespaceListingPermissionDenied"');
|
||||
expect(source).toContain("@click=\"visibleNacosNamespaceAccessMode = 'manual'\"");
|
||||
});
|
||||
|
||||
it("resolves unique manually entered namespace names to their Nacos IDs", () => {
|
||||
expect(source).toContain("function normalizeManualNacosNamespaceNames(namespaces: string[], availableNamespaces: NacosNamespaceInfo[]): string[]");
|
||||
expect(source).toContain("matchingNames.length === 1 ? matchingNames[0] : namespace");
|
||||
expect(source).toContain("async function resolveManualNacosNamespaceNames(namespaces: string[]): Promise<string[]>");
|
||||
expect(source).toContain("managedNamespaces: undefined");
|
||||
expect(source).toContain(": await resolveManualNacosNamespaceNames(manualNamespaces)");
|
||||
});
|
||||
|
||||
it("distinguishes the manual namespace input for the V3 Admin and Console API planes", () => {
|
||||
expect(source).toContain("const isNacosV3ConsolePlane = computed(");
|
||||
expect(source).toContain('isNacosV3AdminPlane.value ? "nacos.nacosManagedNamespaces" : "nacos.nacosManagedNamespacesNameOrId"');
|
||||
expect(source).toContain('isNacosV3AdminPlane.value ? "nacos.nacosManagedNamespacesIdPlaceholder" : "nacos.nacosManagedNamespacesPlaceholder"');
|
||||
expect(source).toContain('if (isNacosV3AdminPlane.value) return "nacos.nacosV3AdminManagedNamespacesHint"');
|
||||
expect(source).toContain('if (isNacosV3ConsolePlane.value) return "nacos.nacosV3ConsoleManagedNamespacesHint"');
|
||||
expect(source).toContain('v-if="isNacosV3AdminPlane" class="flex gap-2 rounded-md border border-amber-500/30');
|
||||
expect(source).toMatch(/const resolvedNamespaces = isNacosV3AdminPlane\.value\s*\?\s*manualNamespaces\s*:\s*await resolveManualNacosNamespaceNames\(manualNamespaces\);/);
|
||||
});
|
||||
|
||||
it("opens namespace access setup instead of showing a save validation error", () => {
|
||||
expect(source).toContain("if (!hasNacosNamespaceScopeForSave()) {");
|
||||
expect(source).toContain("await openVisibleNacosNamespacesPicker();");
|
||||
expect(source).not.toContain('message: t("nacos.nacosNamespaceScopeRequiredBeforeSave")');
|
||||
});
|
||||
|
||||
it("stores automatic full access as a dynamic namespace scope", () => {
|
||||
expect(source).toContain("const nacosDynamicAllNamespaces = ref(false)");
|
||||
expect(source).toContain("const useDynamicAll = selectsEntireReadableList && visibleNacosNamespaceDynamicAllSupported.value");
|
||||
expect(source).toContain("const readableSet = new Set(readableIds.map(nacosNamespaceIdentity))");
|
||||
expect(source).toContain("readableSet.size === sidebarSet.size");
|
||||
expect(source).toContain("form.value.visible_databases = useDynamicAll ? undefined : selected");
|
||||
expect(source).toContain("nacosDynamicAllNamespaces.value = useDynamicAll");
|
||||
expect(source).toContain("function showAllVisibleNacosNamespaces()");
|
||||
});
|
||||
|
||||
it("uses namespaces when scoping Nacos production safeguards", () => {
|
||||
|
||||
@@ -51,6 +51,16 @@ describe("nacosAdmin helpers", () => {
|
||||
contextPath: "/nacos",
|
||||
detectedVersion: "v3",
|
||||
});
|
||||
expect(normalizeNacosEndpoint("http://127.0.0.1:8080", { implementation: "nacos", versionMode: "v3", apiPlane: "console" })).toMatchObject({
|
||||
serverAddr: "http://127.0.0.1:8080",
|
||||
contextPath: "",
|
||||
detectedVersion: "v3",
|
||||
});
|
||||
expect(normalizeNacosEndpoint("http://127.0.0.1:18080/console", { implementation: "nacos", versionMode: "v3", apiPlane: "console" })).toMatchObject({
|
||||
serverAddr: "http://127.0.0.1:18080",
|
||||
contextPath: "/console",
|
||||
detectedVersion: "v3",
|
||||
});
|
||||
expect(normalizeNacosEndpoint("http://127.0.0.1:8848", { implementation: "nacos", versionMode: "v3", contextPath: "/" })).toMatchObject({
|
||||
serverAddr: "http://127.0.0.1:8848",
|
||||
contextPath: "/",
|
||||
|
||||
@@ -60,6 +60,7 @@ export const connectionFinalProxyPort = forward("connectionFinalProxyPort");
|
||||
export const disconnectDb = forward("disconnectDb");
|
||||
export const sessionCredentialStatus = forward("sessionCredentialStatus");
|
||||
export const forgetSessionCredential = forward("forgetSessionCredential");
|
||||
export const replaceNacosSessionCredential = forward("replaceNacosSessionCredential");
|
||||
export const checkConnectionHealth = forward("checkConnectionHealth");
|
||||
export const connectionIdentifierQuote = forward("connectionIdentifierQuote");
|
||||
export const closeDatabaseConnection = forward("closeDatabaseConnection");
|
||||
@@ -368,6 +369,7 @@ export const nacosListNamespaces = forward("nacosListNamespaces");
|
||||
export const nacosSidebarSnapshot = forward("nacosSidebarSnapshot");
|
||||
export const nacosCreateNamespace = forward("nacosCreateNamespace");
|
||||
export const nacosUpdateNamespace = forward("nacosUpdateNamespace");
|
||||
export const nacosDeleteNamespace = forward("nacosDeleteNamespace");
|
||||
export const nacosListConfigs = forward("nacosListConfigs");
|
||||
export const nacosGetConfig = forward("nacosGetConfig");
|
||||
export const nacosPublishConfig = forward("nacosPublishConfig");
|
||||
|
||||
@@ -409,6 +409,10 @@ export async function forgetSessionCredential(connectionId: string): Promise<voi
|
||||
return post("/api/connection/forget-session-credential", { connectionId });
|
||||
}
|
||||
|
||||
export async function replaceNacosSessionCredential(connectionId: string, username: string, password: string): Promise<void> {
|
||||
return post("/api/connection/replace-nacos-session-credential", { connectionId, username, password });
|
||||
}
|
||||
|
||||
export async function checkConnectionHealth(connectionId: string): Promise<void> {
|
||||
return post("/api/connection/check-health", { connectionId });
|
||||
}
|
||||
@@ -3258,8 +3262,8 @@ export async function consulMeshExportedServicesApply(connectionId: string, name
|
||||
// Nacos
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function nacosTestConnection(connectionId: string): Promise<NacosConnectionInfo> {
|
||||
return post("/api/nacos/test-connection", { connectionId });
|
||||
export async function nacosTestConnection(connectionId: string, forceRefresh = false): Promise<NacosConnectionInfo> {
|
||||
return post("/api/nacos/test-connection", { connectionId, forceRefresh });
|
||||
}
|
||||
|
||||
export async function nacosListNamespaces(connectionId: string): Promise<NacosNamespaceInfo[]> {
|
||||
@@ -3278,6 +3282,10 @@ export async function nacosUpdateNamespace(connectionId: string, req: NacosNames
|
||||
return post("/api/nacos/namespaces/update", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeleteNamespace(connectionId: string, namespaceId: string): Promise<void> {
|
||||
return post("/api/nacos/namespaces/delete", { connectionId, namespaceId });
|
||||
}
|
||||
|
||||
export async function nacosListConfigs(connectionId: string, query: NacosConfigQuery): Promise<NacosConfigList> {
|
||||
return post("/api/nacos/configs/list", { connectionId, query });
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ import type {
|
||||
NacosSearchProgress,
|
||||
} from "@/types/nacos";
|
||||
|
||||
export async function nacosTestConnection(connectionId: string): Promise<NacosConnectionInfo> {
|
||||
return invoke("nacos_test_connection", { connectionId });
|
||||
export async function nacosTestConnection(connectionId: string, forceRefresh = false): Promise<NacosConnectionInfo> {
|
||||
return invoke("nacos_test_connection", { connectionId, forceRefresh });
|
||||
}
|
||||
|
||||
export async function nacosListNamespaces(connectionId: string): Promise<NacosNamespaceInfo[]> {
|
||||
@@ -69,6 +69,10 @@ export async function nacosUpdateNamespace(connectionId: string, req: NacosNames
|
||||
return invoke("nacos_update_namespace", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeleteNamespace(connectionId: string, namespaceId: string): Promise<void> {
|
||||
return invoke("nacos_delete_namespace", { connectionId, namespaceId });
|
||||
}
|
||||
|
||||
export async function nacosListConfigs(connectionId: string, query: NacosConfigQuery): Promise<NacosConfigList> {
|
||||
return invoke("nacos_list_configs", { connectionId, query });
|
||||
}
|
||||
|
||||
@@ -953,6 +953,10 @@ export async function forgetSessionCredential(connectionId: string): Promise<voi
|
||||
return invokeBackend("forget_session_credential", { connectionId });
|
||||
}
|
||||
|
||||
export async function replaceNacosSessionCredential(connectionId: string, username: string, password: string): Promise<void> {
|
||||
return invokeBackend("replace_nacos_session_credential", { connectionId, username, password });
|
||||
}
|
||||
|
||||
export async function checkConnectionHealth(connectionId: string): Promise<void> {
|
||||
return invokeBackend("check_connection_health", { connectionId });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NacosConfigHistoryItem, NacosConfigItem, NacosConfigKey, NacosContentMatch, NacosImplementation, NacosInstanceInfo, NacosPermissionInfo, NacosRawRequest, NacosServiceInfo, NacosVersionMode } from "@/types/nacos";
|
||||
import type { NacosApiPlane, NacosConfigHistoryItem, NacosConfigItem, NacosConfigKey, NacosContentMatch, NacosImplementation, NacosInstanceInfo, NacosPermissionInfo, NacosRawRequest, NacosServiceInfo, NacosVersionMode } from "@/types/nacos";
|
||||
import { diffArrays, diffChars } from "diff";
|
||||
|
||||
export type NacosRawTemplateKey = "serverState" | "namespaceList" | "configDetail" | "serviceList" | "instanceList";
|
||||
@@ -58,6 +58,7 @@ export interface NacosEndpointNormalization {
|
||||
export interface NacosEndpointNormalizationOptions {
|
||||
implementation?: NacosImplementation;
|
||||
versionMode?: NacosVersionMode;
|
||||
apiPlane?: NacosApiPlane;
|
||||
contextPath?: string;
|
||||
}
|
||||
|
||||
@@ -78,6 +79,7 @@ export function normalizeNacosEndpoint(input: string, options: NacosEndpointNorm
|
||||
const rawPath = url.pathname.replace(/\/+$/, "");
|
||||
const implementation = options.implementation;
|
||||
const versionMode = options.versionMode || "auto";
|
||||
const apiPlane = options.apiPlane || "admin";
|
||||
const warnings: string[] = [];
|
||||
const hasRNacosSuffix = /\/rnacos$/i.test(rawPath);
|
||||
const hasNacosSuffix = /\/nacos$/i.test(rawPath);
|
||||
@@ -91,7 +93,7 @@ export function normalizeNacosEndpoint(input: string, options: NacosEndpointNorm
|
||||
contextPath = hasNacosSuffix ? rawPath : options.contextPath?.trim() || "/nacos";
|
||||
} else if (detectedVersion === "v3" || hasNacos3UiSuffix) {
|
||||
contextPath = rawPath.replace(/\/(?:next(?:\/index\.html)?|index\.html)$/i, "");
|
||||
if (!contextPath) contextPath = options.contextPath?.trim() || "/nacos";
|
||||
if (!contextPath) contextPath = options.contextPath?.trim() || (apiPlane === "console" ? "" : "/nacos");
|
||||
if (hasNacos3UiSuffix) warnings.push("The Nacos 3 console route was removed from the API context.");
|
||||
} else if (!contextPath) {
|
||||
contextPath = options.contextPath?.trim() || (versionMode === "v2" ? "/nacos" : "");
|
||||
|
||||
@@ -4,7 +4,11 @@ type NacosNamespaceAccessClient = {
|
||||
nacosListNamespaces(connectionId: string): Promise<NacosNamespaceInfo[]>;
|
||||
};
|
||||
|
||||
/** The backend applies account authorization or an explicitly configured namespace scope. */
|
||||
/**
|
||||
* The backend returns namespaces verified through authorization metadata, or,
|
||||
* for this explicit user action, by checking configuration and service reads.
|
||||
* It never returns merely enumerable namespaces as selectable access scope.
|
||||
*/
|
||||
export async function loadReadableNacosNamespaces(connectionId: string, client: NacosNamespaceAccessClient): Promise<NacosNamespaceInfo[]> {
|
||||
return normalizeNacosNamespacesForDisplay(await client.nacosListNamespaces(connectionId));
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface NacosServiceCapabilities {
|
||||
deleteService: NacosOperationCapability;
|
||||
listInstances: NacosOperationCapability;
|
||||
updateInstance: NacosOperationCapability;
|
||||
updateInstanceHealth?: NacosOperationCapability;
|
||||
registerInstance: NacosOperationCapability;
|
||||
deregisterInstance: NacosOperationCapability;
|
||||
}
|
||||
@@ -241,12 +242,14 @@ export interface NacosAuthConfig {
|
||||
|
||||
export type NacosImplementation = "nacos" | "rnacos";
|
||||
export type NacosVersionMode = "auto" | "v2" | "v3";
|
||||
export type NacosApiPlane = "admin" | "console";
|
||||
export type NacosMetricsMode = "auto" | "disabled" | "custom";
|
||||
export type NacosRNacosConsoleAuth = { kind: "inherit" } | { kind: "usernamePassword"; username: string; password: string };
|
||||
|
||||
export interface NacosAdminConfig {
|
||||
implementation?: NacosImplementation;
|
||||
versionMode?: NacosVersionMode;
|
||||
apiPlane?: NacosApiPlane;
|
||||
serverAddr: string;
|
||||
contextPath?: string;
|
||||
/** Namespace IDs used when an official Nacos ordinary user cannot enumerate namespaces or authorization data. */
|
||||
|
||||
Reference in new issue
Block a user