feat(nacos): improve service and instance management
This commit is contained in:
38 files changed
+4787
-695
No files matched your search
+8
-2
@@ -7,13 +7,19 @@ describe("connection dialog scrolling", () => {
|
||||
it("keeps every configuration tab inside a shrinkable form viewport", () => {
|
||||
expect(dialogSource).toContain("return `${widthClass} connection-dialog-content--config`;");
|
||||
expect(dialogSource.match(/<TabsContent[^>]*class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">/g)).toHaveLength(4);
|
||||
expect(dialogSource.match(/class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto/g)).toHaveLength(4);
|
||||
expect(dialogSource.match(/class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto/g)).toHaveLength(4);
|
||||
expect(dialogSource).not.toMatch(/\.connection-dialog-content--config\s*\{[\s\S]*?height:\s*min\(720px/);
|
||||
expect(dialogSource).toMatch(/@media \(max-height: 720px\)[\s\S]*?height:\s*calc\(var\(--dbx-viewport-height\) - 2rem\);/);
|
||||
expect(dialogSource).toMatch(/\.connection-dialog-content--config \.connection-form-body\s*\{[\s\S]*?align-content:\s*start;/);
|
||||
});
|
||||
|
||||
it("keeps the transport form inside a shrinkable scroll viewport", () => {
|
||||
expect(dialogSource).toContain('<TabsContent v-if="canUseTransportLayers" value="transport" class="m-0 flex min-h-0 flex-1 flex-col overflow-hidden">');
|
||||
expect(dialogSource).toContain('class="connection-form-body grid min-h-0 flex-1 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2"');
|
||||
expect(dialogSource).toContain('class="connection-form-body grid min-h-0 flex-1 scroll-pb-6 gap-4 overflow-y-auto overflow-x-hidden pt-4 pr-2 pb-6"');
|
||||
});
|
||||
|
||||
it("keeps conditional Nacos authentication fields from shrinking earlier cards", () => {
|
||||
expect(dialogSource).toContain(":class=\"{ 'connection-form-body--nacos': form.db_type === 'nacos' }\"");
|
||||
expect(dialogSource).toMatch(/\.connection-form-body--nacos\s*\{[\s\S]*?grid-auto-rows:\s*max-content;/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("Nacos connection dialog layout", () => {
|
||||
it("presents implementation and version as one explicit connection profile", () => {
|
||||
expect(source).toContain("data-nacos-profile-selector");
|
||||
expect(source).toContain('v-for="profile in NACOS_CONNECTION_PROFILES"');
|
||||
expect(source).toContain("selectNacosConnectionProfile(profile.value)");
|
||||
expect(source).not.toContain("nacosVersionMode = 'auto'");
|
||||
expect(source).not.toContain("tryNacosDockerConsoleFallback");
|
||||
expect(source).not.toContain("dockerNacosConsoleFallbackUrl");
|
||||
});
|
||||
|
||||
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);
|
||||
const main = source.slice(mainStart, mainEnd);
|
||||
|
||||
expect(main).toContain("data-nacos-endpoint-section");
|
||||
expect(main).toContain("data-nacos-access-section");
|
||||
expect(main).toContain("data-nacos-advanced-hint");
|
||||
expect(main).toContain('t("nacos.nacosAdvancedHint")');
|
||||
expect(main).toContain("@click=\"configTab = 'advanced'\"");
|
||||
expect(main).toContain('v-model="nacosServerAddr"');
|
||||
expect(main).not.toContain('v-model="nacosV3ConsoleAddr"');
|
||||
expect(main).not.toContain('v-model="nacosNamespace"');
|
||||
expect(main).toContain('t("nacos.nacosAuthHint")');
|
||||
expect(main).not.toContain('v-model="nacosMetricsMode"');
|
||||
expect(main).not.toContain('v-model.number="nacosPageSize"');
|
||||
});
|
||||
|
||||
it("moves low-frequency and r-nacos console settings to the advanced tab", () => {
|
||||
const advancedStart = source.indexOf("data-nacos-advanced-settings");
|
||||
const advancedEnd = source.indexOf('v-if="showGaussdbConnectionMode"', advancedStart);
|
||||
const advanced = source.slice(advancedStart, advancedEnd);
|
||||
|
||||
expect(advanced).not.toContain('v-model="nacosContextPathInput"');
|
||||
expect(advanced).not.toContain("配置上下文路径");
|
||||
expect(advanced).toContain('v-model="nacosMetricsMode"');
|
||||
expect(advanced).toContain('v-model="nacosRNacosConsoleAddr"');
|
||||
expect(advanced).toContain('v-if="nacosHistoryEnabled"');
|
||||
expect(advanced).toContain('t("nacos.nacosRnacosDisabledHint")');
|
||||
expect(advanced).toContain('v-model="nacosTlsSkipVerify"');
|
||||
expect(advanced).toContain('v-model.number="nacosPageSize"');
|
||||
});
|
||||
|
||||
it("documents product default ports instead of local Docker mappings", () => {
|
||||
expect(source).toContain('t("nacos.nacosServiceAddressHint")');
|
||||
expect(source).toContain('t("nacos.nacosMetricsHint")');
|
||||
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)).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("openVisibleNacosNamespacesPicker");
|
||||
expect(source).toContain("api.nacosListNamespaces(draftId)");
|
||||
expect(source).toContain("showVisibleNacosNamespacesDialog");
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -195,8 +195,10 @@ describe("nacosAdmin helpers", () => {
|
||||
|
||||
it("includes identifying fields in confirmations", () => {
|
||||
expect(buildNacosConfigDeleteConfirm({ namespace: "", dataId: "app.yaml", group: "DEFAULT_GROUP" })).toContain("dataId=app.yaml");
|
||||
const details = buildNacosInstanceConfirm({ serviceName: "DEFAULT_GROUP@@svc", groupName: "DEFAULT_GROUP" }, { ip: "127.0.0.1", port: 8080, enabled: true, metadata: null }, { enabled: false }, "", "public");
|
||||
const details = buildNacosInstanceConfirm({ serviceName: "DEFAULT_GROUP@@svc", groupName: "DEFAULT_GROUP" }, { ip: "127.0.0.1", port: 8080, clusterName: "blue", ephemeral: false, enabled: true, metadata: null }, { enabled: false }, "", "public");
|
||||
expect(details).toContain("serviceName=DEFAULT_GROUP@@svc");
|
||||
expect(details).toContain("cluster=blue");
|
||||
expect(details).toContain("ephemeral=false");
|
||||
expect(details).toContain("targetEnabled=false");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterNacosNamespacesForSidebar, normalizeNacosNamespaceSelection, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
|
||||
const namespaces = [
|
||||
{ namespace: "", namespaceShowName: "public" },
|
||||
{ namespace: "dev", namespaceShowName: "开发" },
|
||||
{ namespace: "prod", namespaceShowName: "生产" },
|
||||
];
|
||||
|
||||
describe("filterNacosNamespacesForSidebar", () => {
|
||||
it("keeps all namespaces when no visibility filter is configured", () => {
|
||||
expect(filterNacosNamespacesForSidebar(namespaces, undefined)).toEqual(namespaces);
|
||||
});
|
||||
|
||||
it("filters by namespace id and preserves the public empty identifier", () => {
|
||||
expect(filterNacosNamespacesForSidebar(namespaces, ["", "prod"])).toEqual([namespaces[0], namespaces[2]]);
|
||||
});
|
||||
|
||||
it("matches a legacy empty public selection with a concrete public namespace", () => {
|
||||
const v3Namespaces = [{ namespace: "public", namespaceShowName: "public" }, namespaces[2]];
|
||||
expect(filterNacosNamespacesForSidebar(v3Namespaces, ["", "prod"])).toEqual(v3Namespaces);
|
||||
expect(normalizeNacosNamespaceSelection([""], v3Namespaces)).toEqual(["public"]);
|
||||
});
|
||||
|
||||
it("preserves the endpoint-specific public ID when normalizing selections", () => {
|
||||
expect(normalizeNacosNamespaceSelection(["public"], namespaces)).toEqual([""]);
|
||||
});
|
||||
|
||||
it("keeps only the concrete public namespace when both legacy forms are returned", () => {
|
||||
const duplicatePublic = [namespaces[0], { namespace: "public", namespaceShowName: "public" }, namespaces[1]];
|
||||
expect(normalizeNacosNamespacesForDisplay(duplicatePublic)).toEqual([duplicatePublic[1], duplicatePublic[2]]);
|
||||
});
|
||||
});
|
||||
@@ -359,8 +359,14 @@ export const nacosRollbackConfig = forward("nacosRollbackConfig");
|
||||
export const nacosGetRNacosConsoleCaptcha = forward("nacosGetRNacosConsoleCaptcha");
|
||||
export const nacosLoginRNacosConsole = forward("nacosLoginRNacosConsole");
|
||||
export const nacosListServices = forward("nacosListServices");
|
||||
export const nacosGetService = forward("nacosGetService");
|
||||
export const nacosCreateService = forward("nacosCreateService");
|
||||
export const nacosUpdateService = forward("nacosUpdateService");
|
||||
export const nacosDeleteService = forward("nacosDeleteService");
|
||||
export const nacosListInstances = forward("nacosListInstances");
|
||||
export const nacosUpdateInstance = forward("nacosUpdateInstance");
|
||||
export const nacosRegisterInstance = forward("nacosRegisterInstance");
|
||||
export const nacosDeregisterInstance = forward("nacosDeregisterInstance");
|
||||
export const nacosGetDashboard = forward("nacosGetDashboard");
|
||||
export const nacosRawRequest = forward("nacosRawRequest");
|
||||
|
||||
|
||||
@@ -183,8 +183,10 @@ import type {
|
||||
NacosConnectionInfo,
|
||||
NacosRNacosConsoleCaptcha,
|
||||
NacosInstanceInfo,
|
||||
NacosInstanceRef,
|
||||
NacosInstanceRegistration,
|
||||
NacosInstanceQuery,
|
||||
NacosInstanceUpdate,
|
||||
NacosInstanceUpdateRequest,
|
||||
NacosDashboardQuery,
|
||||
NacosDashboardSnapshot,
|
||||
NacosNamespaceCreate,
|
||||
@@ -193,7 +195,9 @@ import type {
|
||||
NacosRawRequest,
|
||||
NacosRawResponse,
|
||||
NacosServiceList,
|
||||
NacosServiceDetail,
|
||||
NacosServiceQuery,
|
||||
NacosServiceUpsert,
|
||||
NacosSearchProgress,
|
||||
} from "@/types/nacos";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
@@ -2806,14 +2810,38 @@ export async function nacosListServices(connectionId: string, query: NacosServic
|
||||
return post("/api/nacos/services/list", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosGetService(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceDetail> {
|
||||
return post("/api/nacos/services/get", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosCreateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return post("/api/nacos/services/create", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosUpdateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return post("/api/nacos/services/update", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeleteService(connectionId: string, query: NacosServiceQuery): Promise<void> {
|
||||
return post("/api/nacos/services/delete", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosListInstances(connectionId: string, query: NacosInstanceQuery): Promise<NacosInstanceInfo[]> {
|
||||
return post("/api/nacos/instances/list", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdate): Promise<void> {
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdateRequest): Promise<void> {
|
||||
return post("/api/nacos/instances/update", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosRegisterInstance(connectionId: string, req: NacosInstanceRegistration): Promise<void> {
|
||||
return post("/api/nacos/instances/register", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeregisterInstance(connectionId: string, req: NacosInstanceRef): Promise<void> {
|
||||
return post("/api/nacos/instances/deregister", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosGetDashboard(connectionId: string, query: NacosDashboardQuery): Promise<NacosDashboardSnapshot> {
|
||||
return post("/api/nacos/dashboard", { connectionId, query });
|
||||
}
|
||||
|
||||
@@ -21,15 +21,19 @@ import type {
|
||||
NacosDashboardSnapshot,
|
||||
NacosRNacosConsoleCaptcha,
|
||||
NacosInstanceInfo,
|
||||
NacosInstanceRef,
|
||||
NacosInstanceRegistration,
|
||||
NacosInstanceQuery,
|
||||
NacosInstanceUpdate,
|
||||
NacosInstanceUpdateRequest,
|
||||
NacosNamespaceCreate,
|
||||
NacosNamespaceInfo,
|
||||
NacosNamespaceUpdate,
|
||||
NacosRawRequest,
|
||||
NacosRawResponse,
|
||||
NacosServiceList,
|
||||
NacosServiceDetail,
|
||||
NacosServiceQuery,
|
||||
NacosServiceUpsert,
|
||||
NacosSearchProgress,
|
||||
} from "@/types/nacos";
|
||||
|
||||
@@ -121,14 +125,38 @@ export async function nacosListServices(connectionId: string, query: NacosServic
|
||||
return invoke("nacos_list_services", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosGetService(connectionId: string, query: NacosServiceQuery): Promise<NacosServiceDetail> {
|
||||
return invoke("nacos_get_service", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosCreateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return invoke("nacos_create_service", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosUpdateService(connectionId: string, req: NacosServiceUpsert): Promise<void> {
|
||||
return invoke("nacos_update_service", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeleteService(connectionId: string, query: NacosServiceQuery): Promise<void> {
|
||||
return invoke("nacos_delete_service", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosListInstances(connectionId: string, query: NacosInstanceQuery): Promise<NacosInstanceInfo[]> {
|
||||
return invoke("nacos_list_instances", { connectionId, query });
|
||||
}
|
||||
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdate): Promise<void> {
|
||||
export async function nacosUpdateInstance(connectionId: string, req: NacosInstanceUpdateRequest): Promise<void> {
|
||||
return invoke("nacos_update_instance", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosRegisterInstance(connectionId: string, req: NacosInstanceRegistration): Promise<void> {
|
||||
return invoke("nacos_register_instance", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosDeregisterInstance(connectionId: string, req: NacosInstanceRef): Promise<void> {
|
||||
return invoke("nacos_deregister_instance", { connectionId, req });
|
||||
}
|
||||
|
||||
export async function nacosGetDashboard(connectionId: string, query: NacosDashboardQuery): Promise<NacosDashboardSnapshot> {
|
||||
return invoke("nacos_get_dashboard", { connectionId, query });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { nacosInstanceMatchesPatch, nacosInstanceRefIdentity, nacosIpAddressIsValid, nacosJsonObjectMatches, nacosServiceDetailMatches } from "../nacosServiceManagement";
|
||||
|
||||
describe("Nacos service management state reconciliation", () => {
|
||||
it("keeps unknown, persistent and ephemeral instances as separate identities", () => {
|
||||
const base = { serviceName: "api", ip: "127.0.0.1", port: 8080, clusterName: "blue" };
|
||||
expect(new Set([nacosInstanceRefIdentity(base), nacosInstanceRefIdentity({ ...base, ephemeral: false }), nacosInstanceRefIdentity({ ...base, ephemeral: true })]).size).toBe(3);
|
||||
});
|
||||
|
||||
it("validates IPv4 and IPv6 instance addresses without accepting host names", () => {
|
||||
expect(nacosIpAddressIsValid("127.0.0.1")).toBe(true);
|
||||
expect(nacosIpAddressIsValid("2001:db8::1")).toBe(true);
|
||||
expect(nacosIpAddressIsValid("999.0.0.1")).toBe(false);
|
||||
expect(nacosIpAddressIsValid("localhost")).toBe(false);
|
||||
});
|
||||
|
||||
it("compares metadata independently of object key order", () => {
|
||||
expect(nacosJsonObjectMatches({ owner: "dbx", nested: { b: 2, a: 1 } }, { nested: { a: 1, b: 2 }, owner: "dbx" })).toBe(true);
|
||||
});
|
||||
|
||||
it("verifies only fields present in an instance patch", () => {
|
||||
const instance = { ip: "127.0.0.1", port: 8080, weight: 0.3, enabled: false, healthy: true, metadata: { role: "api" } };
|
||||
expect(nacosInstanceMatchesPatch(instance, { weight: 0.3000000001, metadata: { role: "api" } })).toBe(true);
|
||||
expect(nacosInstanceMatchesPatch(instance, { metadata: { role: "worker" } })).toBe(false);
|
||||
expect(nacosInstanceMatchesPatch({ ip: "127.0.0.1", port: 8080 }, { weight: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("treats Nacos none selectors as the UI's unconfigured selector", () => {
|
||||
const expected = { serviceName: "api", metadata: { owner: "dbx" }, protectThreshold: 0.5 };
|
||||
expect(nacosServiceDetailMatches({ serviceName: "api", metadata: { owner: "dbx" }, protectThreshold: 0.5, selector: { type: "NoneSelector", contextType: "NONE" } }, expected)).toBe(true);
|
||||
expect(nacosServiceDetailMatches({ serviceName: "api", metadata: { owner: "dbx" } }, expected)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -12,26 +12,26 @@ export interface NacosRawTemplate {
|
||||
}
|
||||
|
||||
export const NACOS_RAW_TEMPLATES: NacosRawTemplate[] = [
|
||||
{ key: "serverState", method: "GET", path: "/v3/console/server/state", query: "", body: "" },
|
||||
{ key: "namespaceList", method: "GET", path: "/v3/console/core/namespace/list", query: "", body: "" },
|
||||
{ key: "serverState", method: "GET", path: "/v3/admin/core/state", query: "", body: "" },
|
||||
{ key: "namespaceList", method: "GET", path: "/v3/admin/core/namespace/list", query: "", body: "" },
|
||||
{
|
||||
key: "configDetail",
|
||||
method: "GET",
|
||||
path: "/v3/console/cs/config",
|
||||
path: "/v3/admin/cs/config",
|
||||
query: "dataId=application.yaml&groupName=DEFAULT_GROUP&namespaceId=",
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
key: "serviceList",
|
||||
method: "GET",
|
||||
path: "/v3/console/ns/service/list",
|
||||
path: "/v3/admin/ns/service/list",
|
||||
query: "pageNo=1&pageSize=20&namespaceId=",
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
key: "instanceList",
|
||||
method: "GET",
|
||||
path: "/v3/console/ns/instance/list",
|
||||
path: "/v3/admin/ns/instance/list",
|
||||
query: "serviceName=DEFAULT_GROUP@@example&namespaceId=",
|
||||
body: "",
|
||||
},
|
||||
@@ -702,8 +702,12 @@ export function buildNacosInstanceConfirm(service: NacosServiceInfo, instance: N
|
||||
`serviceName=${service.serviceName}`,
|
||||
`group=${instance.groupName || service.groupName || fallbackGroup || "DEFAULT_GROUP"}`,
|
||||
`instance=${instance.ip}:${instance.port}`,
|
||||
`cluster=${instance.clusterName || "DEFAULT"}`,
|
||||
`ephemeral=${instance.ephemeral === true ? "true" : instance.ephemeral === false ? "false" : "unknown"}`,
|
||||
patch.enabled == null ? "" : `targetEnabled=${targetEnabled === false ? "false" : "true"}`,
|
||||
patch.healthy == null ? "" : `targetHealthy=${targetHealthy === false ? "false" : "true"}`,
|
||||
patch.weight == null ? "" : `targetWeight=${patch.weight}`,
|
||||
patch.metadata == null ? "" : `targetMetadata=${JSON.stringify(patch.metadata)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { NacosNamespaceInfo } from "@/types/nacos";
|
||||
|
||||
/**
|
||||
* Nacos 2/r-nacos may represent the default namespace with an empty ID, while
|
||||
* Nacos 3 returns the concrete `public` ID. Older backend builds could expose
|
||||
* both forms at once; prefer the concrete ID and keep every other namespace.
|
||||
*/
|
||||
export function normalizeNacosNamespacesForDisplay(namespaces: NacosNamespaceInfo[]): NacosNamespaceInfo[] {
|
||||
const normalized = new Map<string, NacosNamespaceInfo>();
|
||||
for (const namespace of namespaces) {
|
||||
const identity = nacosNamespaceIdentity(namespace.namespace);
|
||||
const existing = normalized.get(identity);
|
||||
// Prefer the concrete public ID when both legacy representations are returned.
|
||||
if (!existing || (identity === "public" && namespace.namespace === "public" && existing.namespace === "")) {
|
||||
normalized.set(identity, namespace);
|
||||
}
|
||||
}
|
||||
return [...normalized.values()];
|
||||
}
|
||||
|
||||
/** Returns the stable display/filter identity without changing the value sent to Nacos. */
|
||||
export function nacosNamespaceIdentity(namespace: string): string {
|
||||
return namespace === "" || namespace === "public" ? "public" : namespace;
|
||||
}
|
||||
|
||||
/** Converts selected identities back to the namespace IDs returned by this Nacos endpoint. */
|
||||
export function normalizeNacosNamespaceSelection(selected: Iterable<string>, namespaces: NacosNamespaceInfo[]): string[] {
|
||||
const available = new Map<string, string>();
|
||||
for (const namespace of normalizeNacosNamespacesForDisplay(namespaces)) {
|
||||
available.set(nacosNamespaceIdentity(namespace.namespace), namespace.namespace);
|
||||
}
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of selected) {
|
||||
const identity = nacosNamespaceIdentity(value);
|
||||
const namespace = available.get(identity);
|
||||
if (namespace !== undefined && !seen.has(identity)) {
|
||||
seen.add(identity);
|
||||
result.push(namespace);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `visible_databases` stores the namespace identifiers selected for a Nacos connection. */
|
||||
export function filterNacosNamespacesForSidebar(namespaces: NacosNamespaceInfo[], visibleNamespaces: string[] | undefined): NacosNamespaceInfo[] {
|
||||
const normalized = normalizeNacosNamespacesForDisplay(namespaces);
|
||||
if (!Array.isArray(visibleNamespaces)) return normalized;
|
||||
const visible = new Set(visibleNamespaces.map(nacosNamespaceIdentity));
|
||||
return normalized.filter((namespace) => visible.has(nacosNamespaceIdentity(namespace.namespace)));
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { NacosInstanceInfo, NacosInstancePatch, NacosInstanceRef, NacosServiceDetail, NacosServiceUpsert } from "@/types/nacos";
|
||||
|
||||
function canonicalJson(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(canonicalJson);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, nested]) => [key, canonicalJson(nested)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function nacosJsonObjectMatches(left: unknown, right: unknown) {
|
||||
return JSON.stringify(canonicalJson(left ?? {})) === JSON.stringify(canonicalJson(right ?? {}));
|
||||
}
|
||||
|
||||
export function nacosIpAddressIsValid(value: string) {
|
||||
const input = value.trim();
|
||||
if (!input) return false;
|
||||
if (input.includes(":")) {
|
||||
try {
|
||||
const parsed = new URL(`http://[${input}]/`);
|
||||
return parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const octets = input.split(".");
|
||||
return octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && (octet === "0" || !octet.startsWith("0")) && Number(octet) <= 255);
|
||||
}
|
||||
|
||||
export function nacosInstanceRefIdentity(ref: NacosInstanceRef) {
|
||||
const lifetime = ref.ephemeral === true ? "ephemeral" : ref.ephemeral === false ? "persistent" : "unknown";
|
||||
return [ref.namespace || "public", ref.groupName || "DEFAULT_GROUP", ref.serviceName, ref.ip, ref.port, ref.clusterName || "DEFAULT", lifetime].join("\u0000");
|
||||
}
|
||||
|
||||
export function nacosInstanceMatchesPatch(instance: NacosInstanceInfo, patch: NacosInstancePatch) {
|
||||
const weightMatches = patch.weight == null || (instance.weight != null && Math.abs(instance.weight - patch.weight) <= 1e-6);
|
||||
const metadataMatches = patch.metadata == null || nacosJsonObjectMatches(instance.metadata, patch.metadata);
|
||||
return (patch.enabled == null || instance.enabled === patch.enabled) && (patch.healthy == null || instance.healthy === patch.healthy) && weightMatches && metadataMatches;
|
||||
}
|
||||
|
||||
export function normalizeNacosSelector(value: unknown): unknown {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length === 0) return null;
|
||||
const selector = value as Record<string, unknown>;
|
||||
const type = String(selector.type ?? "").toLowerCase();
|
||||
const contextType = String(selector.contextType ?? "").toUpperCase();
|
||||
if (type === "none" || type === "noneselector" || contextType === "NONE") return null;
|
||||
return canonicalJson(selector);
|
||||
}
|
||||
|
||||
export function nacosServiceDetailMatches(detail: NacosServiceDetail, expected: NacosServiceUpsert) {
|
||||
const thresholdMatches = expected.protectThreshold == null || (detail.protectThreshold != null && Math.abs(detail.protectThreshold - expected.protectThreshold) <= 1e-6);
|
||||
return nacosJsonObjectMatches(detail.metadata, expected.metadata) && thresholdMatches && JSON.stringify(normalizeNacosSelector(detail.selector)) === JSON.stringify(normalizeNacosSelector(expected.selector));
|
||||
}
|
||||
Reference in new issue
Block a user