fix(kingbase): compat V8 routine source and add extension details
This commit is contained in:
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: "インストール",
|
||||||
|
|||||||
@@ -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: "설치",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: "安装",
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
Reference in new issue
Block a user