36 files changed
+232
-19
No files matched your search
@@ -61,3 +61,53 @@ describe("visibleDatabases schema filtering", () => {
|
||||
expect(isSystemSchemaName("kingbase", "public")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("visible database wildcard patterns (issue #7164)", () => {
|
||||
it("matches names with % and _ wildcards", async () => {
|
||||
const { databaseNameMatchesVisiblePatterns } = await import("@/lib/database/visibleDatabases");
|
||||
expect(databaseNameMatchesVisiblePatterns("n1_order", ["%n1%"])).toBe(true);
|
||||
expect(databaseNameMatchesVisiblePatterns("main", ["%n1%"])).toBe(false);
|
||||
expect(databaseNameMatchesVisiblePatterns("db1", ["db_"])).toBe(true);
|
||||
expect(databaseNameMatchesVisiblePatterns("db12", ["db_"])).toBe(false);
|
||||
expect(databaseNameMatchesVisiblePatterns("a.b", ["a.b"])).toBe(true);
|
||||
expect(databaseNameMatchesVisiblePatterns("axb", ["a.b"])).toBe(false);
|
||||
expect(databaseNameMatchesVisiblePatterns("N1_ORDER", ["%n1%"])).toBe(false);
|
||||
expect(databaseNameMatchesVisiblePatterns("any", [])).toBe(false);
|
||||
expect(databaseNameMatchesVisiblePatterns("any", undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("unions exact selection with pattern matches in filterVisibleDatabaseNames", async () => {
|
||||
const { filterVisibleDatabaseNames } = await import("@/lib/database/visibleDatabases");
|
||||
const names = ["n1_order", "n2_user", "main", "report"];
|
||||
expect(filterVisibleDatabaseNames(names, ["main"], ["%n1%"])).toEqual(["n1_order", "main"]);
|
||||
expect(filterVisibleDatabaseNames(names, undefined, ["n%"])).toEqual(["n1_order", "n2_user"]);
|
||||
expect(filterVisibleDatabaseNames(names, ["main"], undefined)).toEqual(["main"]);
|
||||
expect(filterVisibleDatabaseNames(names, undefined, [])).toEqual(names);
|
||||
});
|
||||
|
||||
it("applies patterns for connections in filterDatabaseNamesForConnection", async () => {
|
||||
const { filterDatabaseNamesForConnection } = await import("@/lib/database/visibleDatabases");
|
||||
const connection = { db_type: "mysql" as const, visible_database_patterns: ["%n1%"] };
|
||||
expect(filterDatabaseNamesForConnection(["n1_order", "main", "mysql", "sys"], connection)).toEqual(["n1_order"]);
|
||||
});
|
||||
|
||||
it("parses user pattern input with separators, trimming and dedupe", async () => {
|
||||
const { parseVisibleDatabasePatternsInput } = await import("@/lib/database/visibleDatabases");
|
||||
expect(parseVisibleDatabasePatternsInput(" %n1%, n2_; \n%n1% ,,")).toEqual(["%n1%", "n2_"]);
|
||||
expect(parseVisibleDatabasePatternsInput(" ")).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats pattern-only configuration as a configured sidebar filter", async () => {
|
||||
const { connectionHasConfiguredSidebarVisibleFilter } = await import("@/lib/sidebar/sidebarVisibleFilterSummary");
|
||||
expect(connectionHasConfiguredSidebarVisibleFilter({ db_type: "mysql", visible_databases: undefined, visible_database_patterns: ["%n1%"] })).toBe(true);
|
||||
expect(connectionHasConfiguredSidebarVisibleFilter({ db_type: "mysql", visible_databases: undefined, visible_database_patterns: [] })).toBe(false);
|
||||
});
|
||||
|
||||
it("counts pattern-matched databases in the sidebar filter summary", async () => {
|
||||
const { sidebarVisibleFilterSummary } = await import("@/lib/sidebar/sidebarVisibleFilterSummary");
|
||||
const summary = sidebarVisibleFilterSummary({ db_type: "mysql", visible_databases: ["main"], visible_database_patterns: ["%n1%"] }, ["main", "n1_order", "n2_user", "mysql"]);
|
||||
expect(summary.isActive).toBe(true);
|
||||
expect(summary.selected).toBe(2);
|
||||
expect(summary.total).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -150,14 +150,57 @@ export function visibleDatabaseFilterIsEnabled(visibleDatabases: string[] | unde
|
||||
return Array.isArray(visibleDatabases);
|
||||
}
|
||||
|
||||
const visibleDatabasePatternRegExpCache = new Map<string, RegExp>();
|
||||
|
||||
/** SQL LIKE 语义:`%` 匹配任意字符序列、`_` 匹配单个字符,其余按字面匹配(区分大小写)。 */
|
||||
export function visibleDatabasePatternRegExp(pattern: string): RegExp {
|
||||
const trimmed = pattern.trim();
|
||||
const cached = visibleDatabasePatternRegExpCache.get(trimmed);
|
||||
if (cached) return cached;
|
||||
let source = "";
|
||||
for (const ch of trimmed) {
|
||||
if (ch === "%") source += "[\\s\\S]*";
|
||||
else if (ch === "_") source += "[\\s\\S]";
|
||||
else source += ch.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
||||
}
|
||||
const regExp = new RegExp(`^${source}$`);
|
||||
if (visibleDatabasePatternRegExpCache.size > 256) visibleDatabasePatternRegExpCache.clear();
|
||||
visibleDatabasePatternRegExpCache.set(trimmed, regExp);
|
||||
return regExp;
|
||||
}
|
||||
|
||||
export function visibleDatabasePatternsAreEnabled(patterns: string[] | undefined): boolean {
|
||||
return Array.isArray(patterns) && patterns.some((pattern) => pattern.trim() !== "");
|
||||
}
|
||||
|
||||
/** 解析用户输入的模式列表:逗号/分号/换行分隔,去空白、去重。 */
|
||||
export function parseVisibleDatabasePatternsInput(value: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
const patterns: string[] = [];
|
||||
for (const part of value.split(/[\n,,;;]+/)) {
|
||||
const pattern = part.trim();
|
||||
if (!pattern || seen.has(pattern)) continue;
|
||||
seen.add(pattern);
|
||||
patterns.push(pattern);
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
export function databaseNameMatchesVisiblePatterns(name: string, patterns: string[] | undefined): boolean {
|
||||
if (!visibleDatabasePatternsAreEnabled(patterns)) return false;
|
||||
return patterns!.some((pattern) => visibleDatabasePatternRegExp(pattern).test(name));
|
||||
}
|
||||
|
||||
export function canSaveVisibleDatabaseSelection(selectedNames: string[]): boolean {
|
||||
return selectedNames.length > 0;
|
||||
}
|
||||
|
||||
export function filterVisibleDatabaseNames(databaseNames: string[], visibleDatabases: string[] | undefined): string[] {
|
||||
if (!visibleDatabaseFilterIsEnabled(visibleDatabases)) return databaseNames;
|
||||
const visible = new Set(visibleDatabases);
|
||||
return databaseNames.filter((name) => visible.has(name));
|
||||
export function filterVisibleDatabaseNames(databaseNames: string[], visibleDatabases: string[] | undefined, visibleDatabasePatterns?: string[]): string[] {
|
||||
const exactEnabled = visibleDatabaseFilterIsEnabled(visibleDatabases);
|
||||
if (!exactEnabled && !visibleDatabasePatternsAreEnabled(visibleDatabasePatterns)) return databaseNames;
|
||||
// 显式勾选与模式取并集:模式对新建的数据库持续生效(#7164)
|
||||
const visible = exactEnabled ? new Set(visibleDatabases) : undefined;
|
||||
return databaseNames.filter((name) => visible?.has(name) || databaseNameMatchesVisiblePatterns(name, visibleDatabasePatterns));
|
||||
}
|
||||
|
||||
export function normalizeVisibleDatabaseSelection(selectedNames: string[], databaseNames: string[]): string[] {
|
||||
@@ -187,8 +230,9 @@ export function isSystemSchemaName(databaseType: DatabaseType | undefined, schem
|
||||
|
||||
export function filterDatabaseNamesForConnection(databaseNames: string[], connection: VisibleDatabaseConnection | undefined): string[] {
|
||||
const visibleDatabases = connection?.visible_databases;
|
||||
if (visibleDatabaseFilterIsEnabled(visibleDatabases)) {
|
||||
return filterVisibleDatabaseNames(databaseNames, visibleDatabases);
|
||||
const visibleDatabasePatterns = connection?.visible_database_patterns;
|
||||
if (visibleDatabaseFilterIsEnabled(visibleDatabases) || visibleDatabasePatternsAreEnabled(visibleDatabasePatterns)) {
|
||||
return filterVisibleDatabaseNames(databaseNames, visibleDatabases, visibleDatabasePatterns);
|
||||
}
|
||||
return filterDatabaseNamesForVisiblePicker(databaseNames, connection);
|
||||
}
|
||||
@@ -218,8 +262,10 @@ export function visibleSchemaFilterIsEnabled(visibleSchemas: Record<string, stri
|
||||
export function filterSchemaNamesForConnection(schemaNames: string[], connection: VisibleDatabaseConnection | undefined, database: string, options?: SchemaFilterOptions): string[] {
|
||||
const visibleSchemas = connection?.visible_schemas;
|
||||
if (!visibleSchemaFilterIsEnabled(visibleSchemas, database)) {
|
||||
if (connectionUsesVisibleSchemaFilter(connection) && visibleDatabaseFilterIsEnabled(connection?.visible_databases)) {
|
||||
return filterVisibleDatabaseNames(schemaNames, connection?.visible_databases);
|
||||
const visibleDatabases = connection?.visible_databases;
|
||||
const visibleDatabasePatterns = connection?.visible_database_patterns;
|
||||
if (connectionUsesVisibleSchemaFilter(connection) && (visibleDatabaseFilterIsEnabled(visibleDatabases) || visibleDatabasePatternsAreEnabled(visibleDatabasePatterns))) {
|
||||
return filterVisibleDatabaseNames(schemaNames, visibleDatabases, visibleDatabasePatterns);
|
||||
}
|
||||
return filterSchemaNamesForVisiblePicker(schemaNames, connection, options);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
|
||||
import { connectionUsesVisibleSchemaFilter, databaseNameMatchesVisiblePatterns, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection, visibleDatabasePatternsAreEnabled } from "@/lib/database/visibleDatabases";
|
||||
import { nacosNamespaceIdentity } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
|
||||
type SidebarVisibleFilterConnection = Pick<ConnectionConfig, "database" | "db_type" | "driver_profile" | "show_system_schemas" | "username" | "visible_databases" | "visible_schemas">;
|
||||
type SidebarVisibleFilterConnection = Pick<ConnectionConfig, "database" | "db_type" | "driver_profile" | "show_system_schemas" | "username" | "visible_databases" | "visible_database_patterns" | "visible_schemas">;
|
||||
|
||||
export type SidebarVisibleFilterSummary = {
|
||||
mode: "database" | "schema" | "namespace";
|
||||
@@ -15,13 +15,15 @@ export function connectionHasConfiguredSidebarVisibleFilter(connection: SidebarV
|
||||
if (connectionUsesVisibleSchemaFilter(connection)) {
|
||||
return Array.isArray(connection.visible_schemas?.[connection.database || ""]);
|
||||
}
|
||||
return Array.isArray(connection.visible_databases);
|
||||
return Array.isArray(connection.visible_databases) || (connection.db_type !== "nacos" && visibleDatabasePatternsAreEnabled(connection.visible_database_patterns));
|
||||
}
|
||||
|
||||
export function sidebarVisibleFilterSummary(connection: SidebarVisibleFilterConnection, objectNames?: readonly string[]): SidebarVisibleFilterSummary {
|
||||
const mode = connectionUsesVisibleSchemaFilter(connection) ? "schema" : "database";
|
||||
const configured = mode === "schema" ? connection.visible_schemas?.[connection.database || ""] : connection.visible_databases;
|
||||
const isExplicit = Array.isArray(configured);
|
||||
// 通配符模式只在数据库模式生效;命中的库名与显式勾选取并集(#7164)
|
||||
const patterns = mode === "database" ? connection.visible_database_patterns : undefined;
|
||||
const isExplicit = Array.isArray(configured) || visibleDatabasePatternsAreEnabled(patterns);
|
||||
if (!objectNames) return { mode, isActive: false, selected: null, total: null };
|
||||
|
||||
const names = [...objectNames];
|
||||
@@ -30,7 +32,13 @@ export function sidebarVisibleFilterSummary(connection: SidebarVisibleFilterConn
|
||||
return { mode, isActive: false, selected: defaultNames.length, total: defaultNames.length };
|
||||
}
|
||||
|
||||
const selectedNames = normalizeVisibleDatabaseSelection(configured, names);
|
||||
const selectedSet = new Set(Array.isArray(configured) ? normalizeVisibleDatabaseSelection(configured, names) : []);
|
||||
if (visibleDatabasePatternsAreEnabled(patterns)) {
|
||||
for (const name of names) {
|
||||
if (databaseNameMatchesVisiblePatterns(name, patterns)) selectedSet.add(name);
|
||||
}
|
||||
}
|
||||
const selectedNames = [...selectedSet];
|
||||
const defaultNameSet = new Set(defaultNames);
|
||||
const includesSystemObject = selectedNames.some((name) => !defaultNameSet.has(name));
|
||||
const total = includesSystemObject ? names.length : defaultNames.length;
|
||||
|
||||
Reference in new issue
Block a user