feat(sidebar): extend filters to views and routines

This commit is contained in:
jearhe authored and GitHub committed 2026-08-23 01:38:19 +08:00
1 parent 3dbcc2d0dc
commit b74d49e982
20 files changed
+268 -46

No files matched your search

@@ -74,6 +74,7 @@ import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
import { connectionUsesVisibleSchemaFilter } from "@/lib/database/visibleDatabases";
import { canTreeNodePin, canTreeNodeShowExpander } from "@/lib/sidebar/sidebarTreeItemLayout";
import { sidebarConnectionVisibleFilterMenu } from "@/lib/sidebar/sidebarVisibleFilterMenu";
import { supportsSidebarObjectNameFilter } from "@/lib/sidebar/sidebarObjectNameFilter";
import { connectionGroupDestinationRows } from "@/lib/sidebar/sidebarLayout";
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
@@ -5658,7 +5659,7 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
disabled: node.isLoading,
});
}
if (node.type === "group-tables") {
if (supportsSidebarObjectNameFilter(node)) {
items.push({
label: t("contextMenu.tableNameFilters"),
action: () => emit("open-table-name-filters", node),
@@ -69,6 +69,7 @@ import { connectionBearingGroupIdsUnder, connectionIdsUnderGroup } from "@/lib/s
import { isSidebarDatabaseOpenForVisual } from "@/lib/sidebar/sidebarDatabaseOpenState";
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
import { connectionCanConfigureSidebarVisibleDatabases } from "@/lib/sidebar/sidebarVisibleFilterMenu";
import { supportsSidebarObjectNameFilter } from "@/lib/sidebar/sidebarObjectNameFilter";
import { isWindows } from "@/lib/backend/platform";
import { flattenTree } from "@/composables/useFlatTree";
import { productionContextForDatabase } from "@/lib/database/productionSafety";
@@ -407,8 +408,8 @@ function visibleLabel(node: TreeNode): string {
return withValidity(displayLabel(node));
}
function hasActiveTableNameFilter(node: TreeNode): boolean {
if (node.type !== "group-tables" || !node.connectionId || !node.database) return false;
function hasActiveObjectNameFilter(node: TreeNode): boolean {
if (!supportsSidebarObjectNameFilter(node) || !node.connectionId || !node.database) return false;
const filter = connectionStore.tableNameFilterForScope({
connectionId: node.connectionId,
database: node.database,
@@ -1471,7 +1472,7 @@ function onKeydown(event: KeyboardEvent) {
node.objectCount != null
"
class="text-muted-foreground text-[10px] shrink-0"
>{{ node.objectCount }}<span v-if="hasActiveTableNameFilter(node)"> · {{ t("tree.tableNameFilterActive") }}</span></span
>{{ node.objectCount }}<span v-if="hasActiveObjectNameFilter(node)"> · {{ t("tree.tableNameFilterActive") }}</span></span
>
<Badge v-if="isNodeDefaultDatabase" variant="secondary" class="h-4 px-1.5 text-[10px]">
{{ t("editor.defaultDatabase") }}
+3 -3
View File
@@ -2783,9 +2783,9 @@ export default {
refreshChildren: "Refresh",
expandAll: "Expand All",
tableNameFilters: "Filters...",
tableNameFiltersDescription: "Show only table names that match the include list, then hide names that match the exclude list. One SQL LIKE pattern per line.",
tableNameFilterInclude: "Include table patterns",
tableNameFilterExclude: "Exclude table patterns",
tableNameFiltersDescription: "Show only object names that match the include list, then hide names that match the exclude list. One SQL LIKE pattern per line.",
tableNameFilterInclude: "Include object name patterns",
tableNameFilterExclude: "Exclude object name patterns",
tableNameFilterIncludePlaceholder: "Example:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "Example:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "Use SQL LIKE syntax: % matches any characters, _ matches one character. Matching is case-insensitive.",
+3 -3
View File
@@ -2716,9 +2716,9 @@ export default withEnglishFallback({
refreshChildren: "Actualizar",
expandAll: "Expandir todo",
tableNameFilters: "Filtros...",
tableNameFiltersDescription: "Muestra solo tablas que coincidan con include y oculta las que coincidan con exclude. Un patrón SQL LIKE por línea.",
tableNameFilterInclude: "Patrones de tablas incluidas",
tableNameFilterExclude: "Patrones de tablas excluidas",
tableNameFiltersDescription: "Muestra solo objetos que coincidan con include y oculta los que coincidan con exclude. Un patrón SQL LIKE por línea.",
tableNameFilterInclude: "Patrones de nombres de objetos incluidos",
tableNameFilterExclude: "Patrones de nombres de objetos excluidos",
tableNameFilterIncludePlaceholder: "Ejemplo:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "Ejemplo:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "Usa sintaxis SQL LIKE: % coincide con cualquier texto, _ con un carácter. No distingue mayúsculas.",
+3 -3
View File
@@ -2714,9 +2714,9 @@ export default withEnglishFallback({
refreshChildren: "Aggiorna",
expandAll: "Espandi tutto",
tableNameFilters: "Filtri...",
tableNameFiltersDescription: "Mostra solo le tabelle che corrispondono alla lista include, poi nasconde quelle che corrispondono alla lista exclude. Un pattern SQL LIKE per riga.",
tableNameFilterInclude: "Pattern tabelle incluse",
tableNameFilterExclude: "Pattern tabelle escluse",
tableNameFiltersDescription: "Mostra solo gli oggetti che corrispondono alla lista include, poi nasconde quelli che corrispondono alla lista exclude. Un pattern SQL LIKE per riga.",
tableNameFilterInclude: "Pattern nomi oggetto inclusi",
tableNameFilterExclude: "Pattern nomi oggetto esclusi",
tableNameFilterIncludePlaceholder: "Esempio:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "Esempio:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "Usa la sintassi SQL LIKE: % corrisponde a qualsiasi testo, _ a un carattere. Match case-insensitive.",
+3 -3
View File
@@ -2738,9 +2738,9 @@ export default withEnglishFallback({
refreshChildren: "更新",
expandAll: "すべて展開",
tableNameFilters: "フィルター...",
tableNameFiltersDescription: "Include リストに一致するテーブル名だけを表示し、Exclude リストに一致する名前を非表示にします。1 行に 1 つの SQL LIKE パターンを入力します。",
tableNameFilterInclude: "含めるテーブル名パターン",
tableNameFilterExclude: "除外するテーブル名パターン",
tableNameFiltersDescription: "Include リストに一致するオブジェクト名だけを表示し、Exclude リストに一致する名前を非表示にします。1 行に 1 つの SQL LIKE パターンを入力します。",
tableNameFilterInclude: "含めるオブジェクト名パターン",
tableNameFilterExclude: "除外するオブジェクト名パターン",
tableNameFilterIncludePlaceholder: "例:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "例:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "SQL LIKE 構文を使用します。% は任意の文字列、_ は 1 文字に一致します。大文字小文字は区別しません。",
+3 -3
View File
@@ -2625,9 +2625,9 @@ export default withEnglishFallback({
refreshChildren: "새로고침",
expandAll: "모두 펼치기",
tableNameFilters: "필터...",
tableNameFiltersDescription: "포함 목록과 일치하는 테이블 이름만 표시한 다음 제외 목록과 일치하는 이름을 숨깁니다. 한 줄에 하나의 SQL LIKE 패턴.",
tableNameFilterInclude: "포함 테이블 패턴",
tableNameFilterExclude: "제외 테이블 패턴",
tableNameFiltersDescription: "포함 목록과 일치하는 객체 이름만 표시한 다음 제외 목록과 일치하는 이름을 숨깁니다. 한 줄에 하나의 SQL LIKE 패턴.",
tableNameFilterInclude: "포함 객체 이름 패턴",
tableNameFilterExclude: "제외 객체 이름 패턴",
tableNameFilterIncludePlaceholder: "예:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "예:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "SQL LIKE 문법을 사용하세요: %는 임의의 문자, _는 한 문자와 일치합니다. 대소문자를 구분하지 않습니다.",
+3 -3
View File
@@ -2716,9 +2716,9 @@ export default withEnglishFallback({
refreshChildren: "Atualizar",
expandAll: "Expandir tudo",
tableNameFilters: "Filtros...",
tableNameFiltersDescription: "Mostra apenas tabelas que correspondem à lista include e oculta as que correspondem à lista exclude. Um padrão SQL LIKE por linha.",
tableNameFilterInclude: "Padrões de tabelas incluídas",
tableNameFilterExclude: "Padrões de tabelas excluídas",
tableNameFiltersDescription: "Mostra apenas objetos que correspondem à lista include e oculta os que correspondem à lista exclude. Um padrão SQL LIKE por linha.",
tableNameFilterInclude: "Padrões de nomes de objetos incluídos",
tableNameFilterExclude: "Padrões de nomes de objetos excluídos",
tableNameFilterIncludePlaceholder: "Exemplo:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "Exemplo:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "Use sintaxe SQL LIKE: % corresponde a qualquer texto, _ a um caractere. A comparação ignora maiúsculas.",
+3 -3
View File
@@ -2707,9 +2707,9 @@ export default withEnglishFallback({
refreshChildren: "刷新",
expandAll: "展开全部",
tableNameFilters: "过滤器...",
tableNameFiltersDescription: "先仅显示命中 include 列表的表名,再隐藏命中 exclude 列表的表名。每行一个 SQL LIKE 模式。",
tableNameFilterInclude: "包含表名模式",
tableNameFilterExclude: "排除表名模式",
tableNameFiltersDescription: "先仅显示命中 include 列表的对象名称,再隐藏命中 exclude 列表的对象名称。每行一个 SQL LIKE 模式。",
tableNameFilterInclude: "包含对象名称模式",
tableNameFilterExclude: "排除对象名称模式",
tableNameFilterIncludePlaceholder: "例如:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "例如:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "使用 SQL LIKE 语法:% 匹配任意字符,_ 匹配单个字符。匹配时不区分大小写。",
+3 -3
View File
@@ -2714,9 +2714,9 @@ export default withEnglishFallback({
refreshChildren: "重新整理",
expandAll: "展開全部",
tableNameFilters: "篩選器...",
tableNameFiltersDescription: "先僅顯示符合 include 清單的資料表名稱,再隱藏符合 exclude 清單的資料表名稱。每行一個 SQL LIKE 模式。",
tableNameFilterInclude: "包含資料表名稱模式",
tableNameFilterExclude: "排除資料表名稱模式",
tableNameFiltersDescription: "先僅顯示符合 include 清單的物件名稱,再隱藏符合 exclude 清單的物件名稱。每行一個 SQL LIKE 模式。",
tableNameFilterInclude: "包含物件名稱模式",
tableNameFilterExclude: "排除物件名稱模式",
tableNameFilterIncludePlaceholder: "範例:\nads_%\nuser_%",
tableNameFilterExcludePlaceholder: "範例:\n%_bak\ntmp_%",
tableNameFilterLikeHint: "使用 SQL LIKE 語法:% 符合任意字元,_ 符合單一字元。比對不區分大小寫。",
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { supportsSidebarObjectNameFilter } from "../../sidebar/sidebarObjectNameFilter";
describe("supportsSidebarObjectNameFilter", () => {
it("allows only supported object groups", () => {
const supportedTypes = ["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions"] as const;
expect(supportedTypes.every((type) => supportsSidebarObjectNameFilter({ type, parentType: undefined }))).toBe(true);
expect(supportsSidebarObjectNameFilter({ type: "group-indexes", parentType: undefined })).toBe(false);
expect(supportsSidebarObjectNameFilter({ type: "group-triggers", parentType: undefined })).toBe(false);
expect(supportsSidebarObjectNameFilter({ type: "group-events", parentType: undefined })).toBe(false);
expect(supportsSidebarObjectNameFilter({ type: "group-functions", parentType: "package" })).toBe(false);
});
});
+2 -1
View File
@@ -829,7 +829,7 @@ export async function getTableComment(_connectionId: string, _database: string,
throw new Error("Table comment lookup is not available in the web backend");
}
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: (SidebarObjectKind | "EVENT")[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: (SidebarObjectKind | "EVENT")[], filter?: string, limit?: number, offset?: number, catalog?: string, tableNameFilter?: TableNameFilter): Promise<ObjectInfo[]> {
return get(
`/api/schema/objects?${qs({
connection_id: connectionId,
@@ -840,6 +840,7 @@ export async function listObjects(connectionId: string, database: string, schema
limit,
offset,
catalog,
table_name_filter: tableNameFilter ? JSON.stringify(tableNameFilter) : undefined,
})}`,
);
}
+2 -1
View File
@@ -1093,7 +1093,7 @@ export async function getTableComment(connectionId: string, database: string, sc
});
}
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: (SidebarObjectKind | "EVENT")[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: (SidebarObjectKind | "EVENT")[], filter?: string, limit?: number, offset?: number, catalog?: string, tableNameFilter?: import("@/types/database").TableNameFilter): Promise<ObjectInfo[]> {
return invoke("list_objects", {
connectionId,
database,
@@ -1103,6 +1103,7 @@ export async function listObjects(connectionId: string, database: string, schema
limit,
offset,
catalog,
tableNameFilter,
});
}
@@ -0,0 +1,12 @@
import type { TreeNode, TreeNodeType } from "@/types/database";
const sidebarObjectNameFilterGroupTypes: ReadonlySet<TreeNodeType> = new Set(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions"]);
type SidebarObjectNameFilterTarget = {
type: TreeNodeType;
parentType?: TreeNode["parentType"];
};
export function supportsSidebarObjectNameFilter(node: SidebarObjectNameFilterTarget): boolean {
return node.parentType !== "package" && sidebarObjectNameFilterGroupTypes.has(node.type);
}
+15 -3
View File
@@ -1715,9 +1715,9 @@ export const useConnectionStore = defineStore("connection", () => {
tableListSourceRevisions.set(connectionId, tableListSourceRevision(connectionId) + 1);
}
function tableNameFilterMetadataExtra(filter: TableNameFilter | undefined, sourceRevision: number): MetadataScopeInput["extra"] {
function tableNameFilterMetadataExtra(filter: TableNameFilter | undefined, sourceRevision?: number): MetadataScopeInput["extra"] {
return {
tableListSourceRevision: sourceRevision,
...(sourceRevision === undefined ? {} : { tableListSourceRevision: sourceRevision }),
...(filter
? {
tableNameFilterInclude: filter.includePatterns,
@@ -2066,6 +2066,14 @@ export const useConnectionStore = defineStore("connection", () => {
return { children: [], objectCount: 0, hasMore: false, nextOffset: options.offset };
}
const searchFilter = options.searchFilter || undefined;
const userTableNameFilter = activeTableNameFilterForScope({
connectionId: options.node.connectionId,
database: options.node.database,
schema: options.node.schema,
nodeKind: options.node.type,
catalog: options.node.catalog,
});
const tableNameFilter = effectiveTableNameFilterForNode(options.node, userTableNameFilter);
const fetchLimit = searchFilter ? undefined : options.pageSize + 1;
const fetchOffset = searchFilter ? undefined : options.offset;
const objects = await loadCachedMetadataListPage<ObjectInfo[]>(
@@ -2080,8 +2088,12 @@ export const useConnectionStore = defineStore("connection", () => {
limit: fetchLimit,
offset: fetchOffset,
sidebarDisplayMode: useSettingsStore().editorSettings.sidebarObjectDisplay,
extra: tableNameFilterMetadataExtra(tableNameFilter),
}),
() => api.listObjects(options.node.connectionId!, options.node.database!, options.querySchema, options.objectTypes, searchFilter, fetchLimit, fetchOffset),
() =>
tableNameFilter || options.node.catalog
? api.listObjects(options.node.connectionId!, options.node.database!, options.querySchema, options.objectTypes, searchFilter, fetchLimit, fetchOffset, options.node.catalog, tableNameFilter)
: api.listObjects(options.node.connectionId!, options.node.database!, options.querySchema, options.objectTypes, searchFilter, fetchLimit, fetchOffset),
{ force: options.force },
);
const hasMore = searchFilter ? false : objects.length > options.pageSize;