fix(kingbase): compat V8 routine source and add extension details

This commit is contained in:
zipg authored and GitHub committed 2026-08-08 10:38:16 +08:00
1 parent 1405f00060
commit fa2365153a
18 files changed
+315 -6

No files matched your search

@@ -100,6 +100,10 @@ func TestKingbaseIntegration(t *testing.T) {
if err != nil || !strings.Contains(fmt.Sprint(source["source"]), function) { if err != nil || !strings.Contains(fmt.Sprint(source["source"]), function) {
t.Fatalf("get function source failed: source=%v err=%v", source, err) t.Fatalf("get function source failed: source=%v err=%v", source, err)
} }
viewSource, err := server.getObjectSource("public", view, "VIEW")
if err != nil || !strings.Contains(fmt.Sprint(viewSource["source"]), parent) {
t.Fatalf("get view source failed: source=%v err=%v", viewSource, err)
}
transactionParams := map[string]json.RawMessage{ transactionParams := map[string]json.RawMessage{
"schema": rawJSON("public"), "schema": rawJSON("public"),
@@ -741,17 +741,35 @@ func (s *server) getObjectSource(schema, name, objectType string) (map[string]an
catalog, prefix, function := "sys_catalog", "sys", "sys_get_viewdef" catalog, prefix, function := "sys_catalog", "sys", "sys_get_viewdef"
if s.mode.postgresCatalog { if s.mode.postgresCatalog {
catalog, prefix, function = "pg_catalog", "pg", "pg_get_viewdef" catalog, prefix, function = "pg_catalog", "pg", "pg_get_viewdef"
} else if s.usePgViewDefinition {
function = "pg_get_viewdef"
}
querySource := func(definitionFunction string) error {
query := fmt.Sprintf("SELECT %s(c.oid) FROM %s.%s_class c JOIN %s.%s_namespace n ON n.oid=c.relnamespace WHERE n.nspname=%s AND c.relname=%s LIMIT 1", definitionFunction, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
return s.requireDBQueryRow(query, &source)
}
err = querySource(function)
if err != nil && function == "sys_get_viewdef" && isUndefinedFunction(err, function) {
s.usePgViewDefinition = true
err = querySource("pg_get_viewdef")
} }
query := fmt.Sprintf("SELECT %s(c.oid) FROM %s.%s_class c JOIN %s.%s_namespace n ON n.oid=c.relnamespace WHERE n.nspname=%s AND c.relname=%s LIMIT 1", function, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
err = s.requireDBQueryRow(query, &source)
} }
} else if kind == "FUNCTION" || kind == "PROCEDURE" { } else if kind == "FUNCTION" || kind == "PROCEDURE" {
catalog, prefix, function := "sys_catalog", "sys", "sys_get_functiondef" catalog, prefix, function := "sys_catalog", "sys", "sys_get_functiondef"
if s.mode.postgresCatalog { if s.mode.postgresCatalog {
catalog, prefix, function = "pg_catalog", "pg", "pg_get_functiondef" catalog, prefix, function = "pg_catalog", "pg", "pg_get_functiondef"
} else if s.usePgFunctionDefinition {
function = "pg_get_functiondef"
}
querySource := func(definitionFunction string) error {
query := fmt.Sprintf("SELECT %s(p.oid) FROM %s.%s_proc p JOIN %s.%s_namespace n ON n.oid=p.pronamespace WHERE n.nspname=%s AND p.proname=%s ORDER BY CASE WHEN p.prorettype=2278 THEN 0 ELSE 1 END LIMIT 1", definitionFunction, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
return s.requireDBQueryRow(query, &source)
}
err = querySource(function)
if err != nil && function == "sys_get_functiondef" && isUndefinedFunction(err, function) {
s.usePgFunctionDefinition = true
err = querySource("pg_get_functiondef")
} }
query := fmt.Sprintf("SELECT %s(p.oid) FROM %s.%s_proc p JOIN %s.%s_namespace n ON n.oid=p.pronamespace WHERE n.nspname=%s AND p.proname=%s ORDER BY CASE WHEN p.prorettype=2278 THEN 0 ELSE 1 END LIMIT 1", function, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
err = s.requireDBQueryRow(query, &source)
} }
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return nil, err return nil, err
+6
View File
@@ -134,6 +134,8 @@ type server struct {
params connectParams params connectParams
mode kingbaseMode mode kingbaseMode
usePgDefaultExpression bool usePgDefaultExpression bool
usePgViewDefinition bool
usePgFunctionDefinition bool
catalogIdentityUnsupported bool catalogIdentityUnsupported bool
infoColumnTypeUnsupported bool infoColumnTypeUnsupported bool
infoUdtNameUnsupported bool infoUdtNameUnsupported bool
@@ -456,6 +458,8 @@ func (s *server) connect(cp connectParams) error {
s.params = cp s.params = cp
s.mode = detectKingbaseMode(db, cp.MySQLCompatMode) s.mode = detectKingbaseMode(db, cp.MySQLCompatMode)
s.usePgDefaultExpression = false s.usePgDefaultExpression = false
s.usePgViewDefinition = false
s.usePgFunctionDefinition = false
s.catalogIdentityUnsupported = false s.catalogIdentityUnsupported = false
s.infoColumnTypeUnsupported = false s.infoColumnTypeUnsupported = false
s.infoUdtNameUnsupported = false s.infoUdtNameUnsupported = false
@@ -520,6 +524,8 @@ func (s *server) disconnect() error {
s.cancelActiveQuery() s.cancelActiveQuery()
s.closeAllQuerySessions() s.closeAllQuerySessions()
s.usePgDefaultExpression = false s.usePgDefaultExpression = false
s.usePgViewDefinition = false
s.usePgFunctionDefinition = false
s.catalogIdentityUnsupported = false s.catalogIdentityUnsupported = false
s.infoColumnTypeUnsupported = false s.infoColumnTypeUnsupported = false
s.infoUdtNameUnsupported = false s.infoUdtNameUnsupported = false
+92
View File
@@ -1142,6 +1142,98 @@ func TestRoutineSourceUsesKingbaseCatalogFunction(t *testing.T) {
} }
} }
func TestObjectSourceFallsBackToPgDefinitionFunctionsAndCachesChoice(t *testing.T) {
tests := []struct {
name string
objectType string
objectName string
sysFunction string
pgFunction string
catalogTable string
source string
}{
{
name: "function",
objectType: "FUNCTION",
objectName: "format_name",
sysFunction: "sys_get_functiondef",
pgFunction: "pg_get_functiondef",
catalogTable: "sys_catalog.sys_proc",
source: "CREATE FUNCTION public.format_name() RETURNS text AS $$ SELECT 'x'; $$",
},
{
name: "view",
objectType: "VIEW",
objectName: "active_orders",
sysFunction: "sys_get_viewdef",
pgFunction: "pg_get_viewdef",
catalogTable: "sys_catalog.sys_class",
source: "SELECT * FROM public.orders WHERE active",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
state := &metadataDriverState{query: func(query string) (driver.Rows, error) {
if !strings.Contains(query, test.catalogTable) {
return nil, errors.New("unexpected query: " + query)
}
if strings.Contains(query, test.sysFunction+"(") {
return nil, &gokb.Error{Code: gokb.ErrorCode("42883"), Message: "function " + test.sysFunction + "(oid) does not exist"}
}
if strings.Contains(query, test.pgFunction+"(") {
return &valueRows{columns: []string{"source"}, rows: [][]driver.Value{{test.source}}}, nil
}
return nil, errors.New("unexpected query: " + query)
}}
server := newServer()
server.db = openMetadataDB(t, state)
for call := 0; call < 2; call++ {
source, err := server.getObjectSource("public", test.objectName, test.objectType)
if err != nil {
t.Fatal(err)
}
if source["source"] != test.source {
t.Fatalf("unexpected source: %#v", source)
}
}
state.mu.Lock()
queries := append([]string(nil), state.queries...)
state.mu.Unlock()
if len(queries) != 3 ||
!strings.Contains(queries[0], test.sysFunction+"(") ||
!strings.Contains(queries[1], test.pgFunction+"(") ||
!strings.Contains(queries[2], test.pgFunction+"(") {
t.Fatalf("definition fallback choice was not cached: %v", queries)
}
})
}
}
func TestObjectSourceDoesNotFallbackOnUnrelatedErrors(t *testing.T) {
permissionErr := errors.New("permission denied for sys_proc")
state := &metadataDriverState{query: func(query string) (driver.Rows, error) {
if strings.Contains(query, "sys_get_functiondef(") {
return nil, permissionErr
}
return nil, errors.New("unexpected fallback query: " + query)
}}
server := newServer()
server.db = openMetadataDB(t, state)
_, err := server.getObjectSource("public", "format_name", "FUNCTION")
if !errors.Is(err, permissionErr) {
t.Fatalf("unexpected error: %v", err)
}
state.mu.Lock()
defer state.mu.Unlock()
if len(state.queries) != 1 || !strings.Contains(state.queries[0], "sys_get_functiondef(") {
t.Fatalf("unrelated error triggered a fallback: %v", state.queries)
}
}
func TestColumnsFallbackToPgGetExprAndCacheChoice(t *testing.T) { func TestColumnsFallbackToPgGetExprAndCacheChoice(t *testing.T) {
registerExpressionFallbackDriver.Do(func() { sql.Register("kingbase-expression-fallback-test", fallbackDriver{}) }) registerExpressionFallbackDriver.Do(func() { sql.Register("kingbase-expression-fallback-test", fallbackDriver{}) })
state := &fallbackDriverState{} state := &fallbackDriverState{}
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { Package } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import type { ExtensionInfo, TreeNode } from "@/types/database";
const { t } = useI18n();
const props = defineProps<{
node: TreeNode;
}>();
const open = ref(false);
const extension = computed<ExtensionInfo>(() => {
const meta = props.node.meta as ExtensionInfo | undefined;
return {
name: meta?.name || props.node.label,
version: meta?.version || "-",
schema: meta?.schema || props.node.schema || null,
comment: meta?.comment || null,
};
});
function show() {
open.value = true;
}
defineExpose({ show });
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle class="flex min-w-0 items-center gap-2 pr-8">
<Package class="h-4 w-4 shrink-0 text-violet-500" />
<span class="truncate">{{ t("extension.detailsTitle") }}</span>
</DialogTitle>
</DialogHeader>
<dl class="overflow-hidden rounded-md border text-sm">
<div class="grid grid-cols-[7rem_minmax(0,1fr)] border-b px-3 py-2.5">
<dt class="text-muted-foreground">{{ t("extension.name") }}</dt>
<dd class="min-w-0 break-words font-medium">{{ extension.name }}</dd>
</div>
<div class="grid grid-cols-[7rem_minmax(0,1fr)] border-b px-3 py-2.5">
<dt class="text-muted-foreground">{{ t("connection.version") }}</dt>
<dd class="min-w-0 break-words">{{ extension.version }}</dd>
</div>
<div class="grid grid-cols-[7rem_minmax(0,1fr)] border-b px-3 py-2.5">
<dt class="text-muted-foreground">Schema</dt>
<dd class="min-w-0 break-words">{{ extension.schema || "-" }}</dd>
</div>
<div class="grid grid-cols-[7rem_minmax(0,1fr)] px-3 py-2.5">
<dt class="text-muted-foreground">{{ t("structureEditor.comment") }}</dt>
<dd class="min-w-0 whitespace-pre-wrap break-words">{{ extension.comment || "-" }}</dd>
</div>
</dl>
<DialogFooter>
<Button variant="outline" @click="open = false">{{ t("common.close") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,80 @@
// @vitest-environment happy-dom
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import type { TreeNode } from "@/types/database";
vi.mock("@/components/ui/dialog", async () => {
const { defineComponent, h } = await import("vue");
const passthrough = defineComponent({
setup(_props, { slots }) {
return () => h("div", slots.default?.());
},
});
const dialog = defineComponent({
props: { open: Boolean },
setup(props, { slots }) {
return () => (props.open ? h("div", slots.default?.()) : null);
},
});
return { Dialog: dialog, DialogContent: passthrough, DialogFooter: passthrough, DialogHeader: passthrough, DialogTitle: passthrough };
});
vi.mock("@/components/ui/button", async () => {
const { defineComponent, h } = await import("vue");
return {
Button: defineComponent({
setup(_props, { slots }) {
return () => h("button", slots.default?.());
},
}),
};
});
import ExtensionDetailsDialog from "@/components/objects/ExtensionDetailsDialog.vue";
const mountedApps: App[] = [];
afterEach(() => {
for (const app of mountedApps.splice(0)) app.unmount();
document.body.innerHTML = "";
});
describe("ExtensionDetailsDialog", () => {
it("shows metadata already loaded for the extension tree node", async () => {
const node: TreeNode = {
id: "connection:database:public:sys_stat_statements",
label: "sys_stat_statements",
type: "extension",
schema: "public",
meta: {
name: "sys_stat_statements",
version: "1.10",
schema: "public",
comment: "track planning and execution statistics",
},
};
const dialog = ref<InstanceType<typeof ExtensionDetailsDialog> | null>(null);
const container = document.createElement("div");
document.body.append(container);
const app = createApp(
defineComponent({
setup() {
return () => h(ExtensionDetailsDialog, { ref: dialog, node });
},
}),
);
mountedApps.push(app);
app.use(i18n);
app.mount(container);
dialog.value?.show();
await nextTick();
const text = document.body.textContent || "";
expect(text).toContain("sys_stat_statements");
expect(text).toContain("1.10");
expect(text).toContain("public");
expect(text).toContain("track planning and execution statistics");
});
});
@@ -29,6 +29,7 @@ import TreeItem from "./TreeItem.vue";
import SidebarTreeRuntimeHost from "./SidebarTreeRuntimeHost.vue"; import SidebarTreeRuntimeHost from "./SidebarTreeRuntimeHost.vue";
import SidebarTreeItemDialogs from "./SidebarTreeItemDialogs.vue"; import SidebarTreeItemDialogs from "./SidebarTreeItemDialogs.vue";
import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog.vue"; import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog.vue";
import ExtensionDetailsDialog from "@/components/objects/ExtensionDetailsDialog.vue";
import { RecycleScroller } from "vue-virtual-scroller"; import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import LightDropdown from "@/components/ui/LightDropdown.vue"; import LightDropdown from "@/components/ui/LightDropdown.vue";
@@ -79,6 +80,8 @@ const sidebarDangerDialogConfirming = ref(false);
const sidebarTreeItemDialogController = ref<Record<string, any> | null>(null); const sidebarTreeItemDialogController = ref<Record<string, any> | null>(null);
const sidebarInstallExtensionTarget = ref<TreeNode | null>(null); const sidebarInstallExtensionTarget = ref<TreeNode | null>(null);
const sidebarInstallExtensionDialogRef = ref<InstanceType<typeof InstallExtensionDialog> | null>(null); const sidebarInstallExtensionDialogRef = ref<InstanceType<typeof InstallExtensionDialog> | null>(null);
const sidebarExtensionDetailsTarget = ref<TreeNode | null>(null);
const sidebarExtensionDetailsDialogRef = ref<InstanceType<typeof ExtensionDetailsDialog> | null>(null);
const sidebarTreeRuntimeHostRef = ref<SidebarTreeRuntimeHostInstance | null>(null); const sidebarTreeRuntimeHostRef = ref<SidebarTreeRuntimeHostInstance | null>(null);
const sidebarTreeRuntime = createSidebarTreeRuntime(); const sidebarTreeRuntime = createSidebarTreeRuntime();
const sidebarTreeRuntimeInitialNode: TreeNode = { id: "__sidebar-runtime__", label: "", type: "connection-group" }; const sidebarTreeRuntimeInitialNode: TreeNode = { id: "__sidebar-runtime__", label: "", type: "connection-group" };
@@ -1284,6 +1287,12 @@ async function openSidebarInstallExtension(node: TreeNode) {
sidebarInstallExtensionDialogRef.value?.show(); sidebarInstallExtensionDialogRef.value?.show();
} }
async function openSidebarExtensionDetails(node: TreeNode) {
sidebarExtensionDetailsTarget.value = createSidebarActionTarget(node);
await nextTick();
sidebarExtensionDetailsDialogRef.value?.show();
}
function beginSidebarAction(): number { function beginSidebarAction(): number {
sidebarActionGeneration += 1; sidebarActionGeneration += 1;
sidebarDdlOpen.value = false; sidebarDdlOpen.value = false;
@@ -1751,6 +1760,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
@open-danger-dialog="openSidebarDangerDialog" @open-danger-dialog="openSidebarDangerDialog"
@open-dialog-controller="updateSidebarTreeItemDialogController" @open-dialog-controller="updateSidebarTreeItemDialogController"
@open-install-extension="openSidebarInstallExtension" @open-install-extension="openSidebarInstallExtension"
@open-extension-details="openSidebarExtensionDetails"
/> />
<div class="connection-tree-search sticky top-0 z-10 bg-background px-2 py-1"> <div class="connection-tree-search sticky top-0 z-10 bg-background px-2 py-1">
<div class="relative flex items-center gap-1"> <div class="relative flex items-center gap-1">
@@ -2056,6 +2066,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
</SidebarDangerConfirmDialog> </SidebarDangerConfirmDialog>
<SidebarTreeItemDialogs v-if="sidebarTreeItemDialogController" :key="sidebarTreeItemDialogController.node?.id" :controller="sidebarTreeItemDialogController" @closed="sidebarTreeItemDialogController = null" /> <SidebarTreeItemDialogs v-if="sidebarTreeItemDialogController" :key="sidebarTreeItemDialogController.node?.id" :controller="sidebarTreeItemDialogController" @closed="sidebarTreeItemDialogController = null" />
<InstallExtensionDialog v-if="sidebarInstallExtensionTarget" ref="sidebarInstallExtensionDialogRef" :node="sidebarInstallExtensionTarget" @close="refreshSidebarActionTarget" @changed="refreshSidebarActionTarget" /> <InstallExtensionDialog v-if="sidebarInstallExtensionTarget" ref="sidebarInstallExtensionDialogRef" :node="sidebarInstallExtensionTarget" @close="refreshSidebarActionTarget" @changed="refreshSidebarActionTarget" />
<ExtensionDetailsDialog v-if="sidebarExtensionDetailsTarget" ref="sidebarExtensionDetailsDialogRef" :node="sidebarExtensionDetailsTarget" />
<div v-if="store.treeNodes.length === 0" class="px-3 py-8 text-center text-muted-foreground text-xs"> <div v-if="store.treeNodes.length === 0" class="px-3 py-8 text-center text-muted-foreground text-xs">
{{ t("sidebar.noConnections") }} {{ t("sidebar.noConnections") }}
</div> </div>
@@ -342,6 +342,7 @@ const emit = defineEmits<{
"open-danger-dialog": [request: SidebarDangerDialogRequest]; "open-danger-dialog": [request: SidebarDangerDialogRequest];
"open-dialog-controller": [controller: Record<string, any> | null]; "open-dialog-controller": [controller: Record<string, any> | null];
"open-install-extension": [node: TreeNode]; "open-install-extension": [node: TreeNode];
"open-extension-details": [node: TreeNode];
}>(); }>();
const { const {
@@ -760,6 +761,8 @@ function runRowClickAction(clickDetail: number) {
scheduleOpenData(node); scheduleOpenData(node);
} else if (action === "open-source") { } else if (action === "open-source") {
openObjectSourceDialog(false); openObjectSourceDialog(false);
} else if (action === "open-extension-details") {
emit("open-extension-details", node);
} else if (isDocumentBrowserTreeNode(node.type)) { } else if (isDocumentBrowserTreeNode(node.type)) {
openMongoTreeData(node); openMongoTreeData(node);
} else if (action === "toggle") { } else if (action === "toggle") {
@@ -1067,6 +1070,8 @@ function onDoubleClick(event: MouseEvent) {
activateDataTableFromDoubleClick(); activateDataTableFromDoubleClick();
} else if (action === "open-source") { } else if (action === "open-source") {
openObjectSourceDialog(false); openObjectSourceDialog(false);
} else if (action === "open-extension-details") {
emit("open-extension-details", activeNode.value);
} else if (action === "open-saved-sql") { } else if (action === "open-saved-sql") {
openSavedSqlFile(); openSavedSqlFile();
} else if (action === "toggle" && (activeNode.value.type === "mongo-gridfs" || isDocumentBrowserTreeNode(activeNode.value.type))) { } else if (action === "toggle" && (activeNode.value.type === "mongo-gridfs" || isDocumentBrowserTreeNode(activeNode.value.type))) {
@@ -4395,6 +4400,8 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
// 8.5 Extension // 8.5 Extension
if (node.type === "extension") { if (node.type === "extension") {
items.push({ label: t("extension.viewDetails"), action: () => emit("open-extension-details", node), icon: Info });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
return true; return true;
} }
+2
View File
@@ -2566,6 +2566,8 @@ export default {
extension: { extension: {
manageTitle: "Manage Extensions", manageTitle: "Manage Extensions",
installTitle: "Install Extension", installTitle: "Install Extension",
detailsTitle: "Extension Details",
viewDetails: "View details",
name: "Extension name", name: "Extension name",
namePlaceholder: "e.g. pg_stat_statements", namePlaceholder: "e.g. pg_stat_statements",
install: "Install", install: "Install",
+2
View File
@@ -5933,6 +5933,8 @@ export default withEnglishFallback({
extension: { extension: {
installTitle: "Instalar extensión", installTitle: "Instalar extensión",
manageTitle: "Administrar extensión", manageTitle: "Administrar extensión",
detailsTitle: "Detalles de la extensión",
viewDetails: "Ver detalles",
name: "Nombre de la extensión", name: "Nombre de la extensión",
namePlaceholder: "Por ejemplo: pg_stat_statements", namePlaceholder: "Por ejemplo: pg_stat_statements",
install: "Instalar", install: "Instalar",
+2
View File
@@ -5933,6 +5933,8 @@ export default withEnglishFallback({
extension: { extension: {
installTitle: "Installa estensione", installTitle: "Installa estensione",
manageTitle: "Gestisci estensioni", manageTitle: "Gestisci estensioni",
detailsTitle: "Dettagli estensione",
viewDetails: "Visualizza dettagli",
name: "Nome estensione", name: "Nome estensione",
namePlaceholder: "Es: pg_stat_statements", namePlaceholder: "Es: pg_stat_statements",
install: "Installa", install: "Installa",
+2
View File
@@ -5988,6 +5988,8 @@ export default withEnglishFallback({
extension: { extension: {
installTitle: "拡張機能のインストール", installTitle: "拡張機能のインストール",
manageTitle: "拡張機能の管理", manageTitle: "拡張機能の管理",
detailsTitle: "拡張機能の詳細",
viewDetails: "詳細を表示",
name: "拡張機能名", name: "拡張機能名",
namePlaceholder: "例: pg_stat_statements", namePlaceholder: "例: pg_stat_statements",
install: "インストール", install: "インストール",
+2
View File
@@ -2440,6 +2440,8 @@ export default withEnglishFallback({
extension: { extension: {
manageTitle: "확장 관리", manageTitle: "확장 관리",
installTitle: "확장 설치", installTitle: "확장 설치",
detailsTitle: "확장 세부 정보",
viewDetails: "세부 정보 보기",
name: "확장 이름", name: "확장 이름",
namePlaceholder: "예: pg_stat_statements", namePlaceholder: "예: pg_stat_statements",
install: "설치", install: "설치",
+2
View File
@@ -5935,6 +5935,8 @@ export default withEnglishFallback({
extension: { extension: {
installTitle: "Instalar extensão", installTitle: "Instalar extensão",
manageTitle: "Gerenciar extensão", manageTitle: "Gerenciar extensão",
detailsTitle: "Detalhes da extensão",
viewDetails: "Ver detalhes",
name: "Nome da extensão", name: "Nome da extensão",
namePlaceholder: "Exemplo: pg_stat_statements", namePlaceholder: "Exemplo: pg_stat_statements",
install: "Instalar", install: "Instalar",
+2
View File
@@ -2566,6 +2566,8 @@ export default withEnglishFallback({
extension: { extension: {
installTitle: "安装扩展", installTitle: "安装扩展",
manageTitle: "管理扩展", manageTitle: "管理扩展",
detailsTitle: "扩展详情",
viewDetails: "查看详情",
name: "扩展名称", name: "扩展名称",
namePlaceholder: "例如: pg_stat_statements", namePlaceholder: "例如: pg_stat_statements",
install: "安装", install: "安装",
+2
View File
@@ -5928,6 +5928,8 @@ export default withEnglishFallback({
extension: { extension: {
installTitle: "安裝擴展", installTitle: "安裝擴展",
manageTitle: "管理擴展", manageTitle: "管理擴展",
detailsTitle: "擴展詳情",
viewDetails: "查看詳情",
name: "擴展名稱", name: "擴展名稱",
namePlaceholder: "例如:pg_stat_statements", namePlaceholder: "例如:pg_stat_statements",
install: "安裝", install: "安裝",
@@ -1,8 +1,8 @@
import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database"; import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database";
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts"; import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
export type TreeNodeRowAction = "open-data" | "open-source" | "toggle" | "none"; export type TreeNodeRowAction = "open-data" | "open-source" | "open-extension-details" | "toggle" | "none";
export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none"; export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-extension-details" | "open-saved-sql" | "toggle" | "none";
export type SidebarSelectionCopyAction = "copy-name" | "none"; export type SidebarSelectionCopyAction = "copy-name" | "none";
export type SidebarActivation = "single" | "double"; export type SidebarActivation = "single" | "double";
@@ -36,6 +36,7 @@ export function isDocumentBrowserTreeNode(type: TreeNodeType): boolean {
export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean, activation: SidebarActivation = "single"): TreeNodeRowAction { export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean, activation: SidebarActivation = "single"): TreeNodeRowAction {
if (activation === "double") return "none"; if (activation === "double") return "none";
if (type === "extension") return "open-extension-details";
if (dataNodeTypes.has(type)) return "open-data"; if (dataNodeTypes.has(type)) return "open-data";
if (sourceNodeTypes.has(type)) return "open-source"; if (sourceNodeTypes.has(type)) return "open-source";
if (toggleLeafNodeTypes.has(type)) return "toggle"; if (toggleLeafNodeTypes.has(type)) return "toggle";
@@ -55,6 +56,7 @@ export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBr
// sequence. Only double-click activation needs a second-stage table action. // sequence. Only double-click activation needs a second-stage table action.
if (type === "table") return activation === "double" ? "activate-data" : "none"; if (type === "table") return activation === "double" ? "activate-data" : "none";
if (activation === "double") { if (activation === "double") {
if (type === "extension") return "open-extension-details";
if (dataNodeTypes.has(type)) return "open-data"; if (dataNodeTypes.has(type)) return "open-data";
if (sourceNodeTypes.has(type)) return "open-source"; if (sourceNodeTypes.has(type)) return "open-source";
if (savedSqlNodeTypes.has(type)) return "open-saved-sql"; if (savedSqlNodeTypes.has(type)) return "open-saved-sql";
+6
View File
@@ -16,6 +16,12 @@ test("single click navigation mode opens source-capable rows", () => {
assert.equal(treeNodeRowAction("sequence", false), "open-source"); assert.equal(treeNodeRowAction("sequence", false), "open-source");
}); });
test("extension rows open their metadata details", () => {
assert.equal(treeNodeRowAction("extension", false, "single"), "open-extension-details");
assert.equal(treeNodeRowAction("extension", false, "double"), "none");
assert.equal(treeNodeRowDoubleClickAction("extension", false, "double"), "open-extension-details");
});
test("double click navigation mode selects rows on single click", () => { test("double click navigation mode selects rows on single click", () => {
assert.equal(treeNodeRowAction("table", true, "double"), "none"); assert.equal(treeNodeRowAction("table", true, "double"), "none");
assert.equal(treeNodeRowAction("view", true, "double"), "none"); assert.equal(treeNodeRowAction("view", true, "double"), "none");