feat(objects): show routines in object browser
This commit is contained in:
1 parent
d4afbc7b58
commit
6d513afc16
15 files changed
+269
-30
No files matched your search
@@ -1,24 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
import { Loader2, RefreshCw, Search, Table2, Eye } from "lucide-vue-next";
|
||||
import { Braces, Eye, Loader2, RefreshCw, Search, ScrollText, Table2 } from "lucide-vue-next";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import * as api from "@/lib/api";
|
||||
import type { ConnectionConfig, TableInfo } from "@/types/database";
|
||||
import type { ConnectionConfig, ObjectInfo } from "@/types/database";
|
||||
import { isSchemaAware } from "@/lib/databaseCapabilities";
|
||||
|
||||
type ObjectRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
schema?: string;
|
||||
type: "TABLE" | "VIEW";
|
||||
type: "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION";
|
||||
comment?: string | null;
|
||||
};
|
||||
|
||||
type ObjectFilter = "all" | "tables" | "views";
|
||||
type ObjectFilter = "all" | "tables" | "views" | "procedures" | "functions";
|
||||
|
||||
const props = defineProps<{
|
||||
connection: ConnectionConfig;
|
||||
@@ -46,7 +46,22 @@ let loadId = 0;
|
||||
const needsSchema = computed(() => isSchemaAware(props.connection.db_type));
|
||||
const tableCount = computed(() => rows.value.filter((row) => row.type === "TABLE").length);
|
||||
const viewCount = computed(() => rows.value.filter((row) => row.type === "VIEW").length);
|
||||
const showObjectFilter = computed(() => tableCount.value > 0 && viewCount.value > 0);
|
||||
const procedureCount = computed(() => rows.value.filter((row) => row.type === "PROCEDURE").length);
|
||||
const functionCount = computed(() => rows.value.filter((row) => row.type === "FUNCTION").length);
|
||||
const objectFilters = computed<ObjectFilter[]>(() =>
|
||||
(
|
||||
[
|
||||
["all", rows.value.length],
|
||||
["tables", tableCount.value],
|
||||
["views", viewCount.value],
|
||||
["procedures", procedureCount.value],
|
||||
["functions", functionCount.value],
|
||||
] as Array<[ObjectFilter, number]>
|
||||
)
|
||||
.filter(([filter, count]) => filter === "all" || count > 0)
|
||||
.map(([filter]) => filter),
|
||||
);
|
||||
const showObjectFilter = computed(() => objectFilters.value.length > 2);
|
||||
const hasComments = computed(() => rows.value.some((row) => row.comment?.trim()));
|
||||
const gridTemplateColumns = computed(() =>
|
||||
hasComments.value ? "minmax(0,1fr) 120px 160px minmax(160px,0.7fr)" : "minmax(0,1fr) 120px 160px",
|
||||
@@ -63,21 +78,47 @@ const searchedRows = computed(() => {
|
||||
const filteredRows = computed(() => {
|
||||
if (objectFilter.value === "tables") return searchedRows.value.filter((row) => row.type === "TABLE");
|
||||
if (objectFilter.value === "views") return searchedRows.value.filter((row) => row.type === "VIEW");
|
||||
if (objectFilter.value === "procedures") return searchedRows.value.filter((row) => row.type === "PROCEDURE");
|
||||
if (objectFilter.value === "functions") return searchedRows.value.filter((row) => row.type === "FUNCTION");
|
||||
return searchedRows.value;
|
||||
});
|
||||
|
||||
function normalizeType(type: string): ObjectRow["type"] {
|
||||
const value = type.toUpperCase();
|
||||
if (value.includes("VIEW")) return "VIEW";
|
||||
if (value.includes("PROC")) return "PROCEDURE";
|
||||
if (value.includes("FUNC")) return "FUNCTION";
|
||||
return "TABLE";
|
||||
}
|
||||
|
||||
function iconFor(row: ObjectRow) {
|
||||
return row.type === "VIEW" ? Eye : Table2;
|
||||
if (row.type === "VIEW") return Eye;
|
||||
if (row.type === "PROCEDURE") return ScrollText;
|
||||
if (row.type === "FUNCTION") return Braces;
|
||||
return Table2;
|
||||
}
|
||||
|
||||
function typeLabel(type: ObjectRow["type"]) {
|
||||
return type === "VIEW" ? t("objects.view") : t("objects.table");
|
||||
if (type === "VIEW") return t("objects.view");
|
||||
if (type === "PROCEDURE") return t("objects.procedure");
|
||||
if (type === "FUNCTION") return t("objects.function");
|
||||
return t("objects.table");
|
||||
}
|
||||
|
||||
function iconClass(type: ObjectRow["type"]) {
|
||||
if (type === "VIEW") return "text-purple-500";
|
||||
if (type === "PROCEDURE") return "text-blue-500";
|
||||
if (type === "FUNCTION") return "text-amber-500";
|
||||
return "text-green-500";
|
||||
}
|
||||
|
||||
function canOpen(row: ObjectRow) {
|
||||
return row.type === "TABLE" || row.type === "VIEW";
|
||||
}
|
||||
|
||||
function openRow(row: ObjectRow) {
|
||||
if (!canOpen(row)) return;
|
||||
emit("openTable", { tableName: row.name, schema: row.schema });
|
||||
}
|
||||
|
||||
async function loadSchemas() {
|
||||
@@ -105,14 +146,14 @@ async function loadObjects() {
|
||||
rows.value = [];
|
||||
try {
|
||||
const schema = needsSchema.value ? selectedSchema.value || "" : props.database;
|
||||
const tables: TableInfo[] = await api.listTables(props.connection.id, props.database, schema);
|
||||
const objects: ObjectInfo[] = await api.listObjects(props.connection.id, props.database, schema);
|
||||
if (id !== loadId) return;
|
||||
rows.value = tables.map((table) => ({
|
||||
id: `${schema || props.database}:${table.name}:${table.table_type}`,
|
||||
name: table.name,
|
||||
schema: needsSchema.value ? schema : undefined,
|
||||
type: normalizeType(table.table_type),
|
||||
comment: table.comment,
|
||||
rows.value = objects.map((object) => ({
|
||||
id: `${object.schema || schema || props.database}:${object.name}:${object.object_type}`,
|
||||
name: object.name,
|
||||
schema: object.schema || (needsSchema.value ? schema : undefined),
|
||||
type: normalizeType(object.object_type),
|
||||
comment: object.comment,
|
||||
}));
|
||||
} catch (e: any) {
|
||||
if (id !== loadId) return;
|
||||
@@ -136,11 +177,22 @@ function onSchemaChange(value: any) {
|
||||
function filterCount(filter: ObjectFilter) {
|
||||
if (filter === "tables") return tableCount.value;
|
||||
if (filter === "views") return viewCount.value;
|
||||
if (filter === "procedures") return procedureCount.value;
|
||||
if (filter === "functions") return functionCount.value;
|
||||
return rows.value.length;
|
||||
}
|
||||
|
||||
function filterLabel(filter: ObjectFilter) {
|
||||
const key = filter === "tables" ? "objects.tables" : filter === "views" ? "objects.views" : "objects.all";
|
||||
const key =
|
||||
filter === "tables"
|
||||
? "objects.tables"
|
||||
: filter === "views"
|
||||
? "objects.views"
|
||||
: filter === "procedures"
|
||||
? "objects.procedures"
|
||||
: filter === "functions"
|
||||
? "objects.functions"
|
||||
: "objects.all";
|
||||
return `${t(key)} ${filterCount(filter)}`;
|
||||
}
|
||||
|
||||
@@ -171,7 +223,7 @@ watch(
|
||||
<Input v-model="search" class="h-7 text-xs" :placeholder="t('objects.search')" />
|
||||
<div v-if="showObjectFilter" class="flex h-7 shrink-0 items-center rounded border bg-muted/20 p-0.5">
|
||||
<button
|
||||
v-for="filter in ['all', 'tables', 'views'] as ObjectFilter[]"
|
||||
v-for="filter in objectFilters"
|
||||
:key="filter"
|
||||
type="button"
|
||||
class="h-6 rounded-sm px-2 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
@@ -223,24 +275,21 @@ watch(
|
||||
<div class="truncate">{{ t("objects.schemaColumn") }}</div>
|
||||
<div v-if="hasComments" class="truncate">{{ t("objects.comment") }}</div>
|
||||
</div>
|
||||
<RecycleScroller class="flex-1 min-h-0" :items="filteredRows" :item-size="36" key-field="id">
|
||||
<RecycleScroller class="flex-1 min-h-0" :items="filteredRows" :item-size="38" key-field="id">
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
class="grid h-9 cursor-pointer items-center gap-3 border-b px-3 text-xs hover:bg-accent/50"
|
||||
class="grid h-[38px] cursor-pointer items-center gap-3 border-b px-3 hover:bg-accent/50"
|
||||
:class="{ 'cursor-default hover:bg-transparent': !canOpen(item) }"
|
||||
:style="{ gridTemplateColumns }"
|
||||
@click="emit('openTable', { tableName: item.name, schema: item.schema })"
|
||||
@click="openRow(item)"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<component
|
||||
:is="iconFor(item)"
|
||||
class="h-3.5 w-3.5 shrink-0"
|
||||
:class="item.type === 'VIEW' ? 'text-purple-500' : 'text-green-500'"
|
||||
/>
|
||||
<span class="truncate font-medium">{{ item.name }}</span>
|
||||
<component :is="iconFor(item)" class="h-3.5 w-3.5 shrink-0" :class="iconClass(item.type)" />
|
||||
<span class="truncate text-[13px] font-medium text-foreground">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="truncate text-muted-foreground">{{ typeLabel(item.type) }}</div>
|
||||
<div class="truncate text-muted-foreground">{{ item.schema || props.database }}</div>
|
||||
<div v-if="hasComments" class="truncate text-muted-foreground" :title="item.comment || ''">
|
||||
<div class="truncate text-xs text-muted-foreground">{{ typeLabel(item.type) }}</div>
|
||||
<div class="truncate text-xs text-muted-foreground">{{ item.schema || props.database }}</div>
|
||||
<div v-if="hasComments" class="truncate text-xs text-muted-foreground" :title="item.comment || ''">
|
||||
{{ item.comment || "" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -549,15 +549,19 @@ export default {
|
||||
all: "All",
|
||||
tables: "Tables",
|
||||
views: "Views",
|
||||
procedures: "Procedures",
|
||||
functions: "Functions",
|
||||
table: "Table",
|
||||
view: "View",
|
||||
procedure: "Procedure",
|
||||
function: "Function",
|
||||
name: "Name",
|
||||
type: "Type",
|
||||
schemaColumn: "Schema",
|
||||
comment: "Comment",
|
||||
loadingSchemas: "Loading schemas...",
|
||||
schema: "Schema",
|
||||
search: "Search tables or views...",
|
||||
search: "Search tables, views, functions, or procedures...",
|
||||
loading: "Loading objects...",
|
||||
empty: "No objects found",
|
||||
},
|
||||
|
||||
@@ -538,15 +538,19 @@ export default {
|
||||
all: "全部",
|
||||
tables: "表",
|
||||
views: "视图",
|
||||
procedures: "存储过程",
|
||||
functions: "函数",
|
||||
table: "表",
|
||||
view: "视图",
|
||||
procedure: "存储过程",
|
||||
function: "函数",
|
||||
name: "名称",
|
||||
type: "类型",
|
||||
schemaColumn: "Schema",
|
||||
comment: "注释",
|
||||
loadingSchemas: "加载 Schema...",
|
||||
schema: "Schema",
|
||||
search: "搜索表或视图...",
|
||||
search: "搜索表、视图、函数或存储过程...",
|
||||
loading: "正在加载对象...",
|
||||
empty: "没有找到对象",
|
||||
},
|
||||
|
||||
@@ -56,6 +56,7 @@ export const loadSchemaCache = forward("loadSchemaCache");
|
||||
export const deleteSchemaCachePrefix = forward("deleteSchemaCachePrefix");
|
||||
export const listSchemas = forward("listSchemas");
|
||||
export const listTables = forward("listTables");
|
||||
export const listObjects = forward("listObjects");
|
||||
export const getColumns = forward("getColumns");
|
||||
export const listIndexes = forward("listIndexes");
|
||||
export const listForeignKeys = forward("listForeignKeys");
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
ConnectionConfig,
|
||||
DatabaseInfo,
|
||||
TableInfo,
|
||||
ObjectInfo,
|
||||
ColumnInfo,
|
||||
IndexInfo,
|
||||
ForeignKeyInfo,
|
||||
@@ -170,6 +171,10 @@ export async function listTables(connectionId: string, database: string, schema:
|
||||
return get(`/api/schema/tables?${qs({ connection_id: connectionId, database, schema })}`);
|
||||
}
|
||||
|
||||
export async function listObjects(connectionId: string, database: string, schema: string): Promise<ObjectInfo[]> {
|
||||
return get(`/api/schema/objects?${qs({ connection_id: connectionId, database, schema })}`);
|
||||
}
|
||||
|
||||
export async function getColumns(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ConnectionConfig,
|
||||
DatabaseInfo,
|
||||
TableInfo,
|
||||
ObjectInfo,
|
||||
ColumnInfo,
|
||||
IndexInfo,
|
||||
ForeignKeyInfo,
|
||||
@@ -139,6 +140,10 @@ export async function listTables(connectionId: string, database: string, schema:
|
||||
return invoke("list_tables", { connectionId, database, schema });
|
||||
}
|
||||
|
||||
export async function listObjects(connectionId: string, database: string, schema: string): Promise<ObjectInfo[]> {
|
||||
return invoke("list_objects", { connectionId, database, schema });
|
||||
}
|
||||
|
||||
export async function listSchemas(connectionId: string, database: string): Promise<string[]> {
|
||||
return invoke("list_schemas", { connectionId, database });
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@ export interface TableInfo {
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
export type DatabaseObjectType = "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION";
|
||||
|
||||
export interface ObjectInfo {
|
||||
name: string;
|
||||
object_type: DatabaseObjectType | string;
|
||||
schema?: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
export interface ColumnInfo {
|
||||
name: string;
|
||||
data_type: string;
|
||||
|
||||
Reference in new issue
Block a user