feat: add SSH config aliases and PostgreSQL extensions
This commit is contained in:
1 parent
8b035d0989
commit
f800d80f90
35 files changed
+1191
-33
No files matched your search
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Loader2, Package, Plus, Trash2 } 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 { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { buildCreateExtensionSql, buildDropExtensionSql } from "@/lib/database/dbAdminSql";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import type { ExtensionInfo, TreeNode } from "@/types/database";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
|
||||
const props = defineProps<{
|
||||
node: TreeNode;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const open = ref(false);
|
||||
const available = ref<ExtensionInfo[]>([]);
|
||||
const installed = ref<ExtensionInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const installing = ref<string | null>(null);
|
||||
const dropping = ref<string | null>(null);
|
||||
|
||||
function show() {
|
||||
open.value = true;
|
||||
void loadData();
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!props.node.connectionId || !props.node.database) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const [avail, inst] = await Promise.all([api.listAvailableExtensions(props.node.connectionId, props.node.database).catch(() => [] as ExtensionInfo[]), api.listExtensions(props.node.connectionId, props.node.database, props.node.schema || "public").catch(() => [] as ExtensionInfo[])]);
|
||||
available.value = avail;
|
||||
installed.value = inst;
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function installExtension(name: string) {
|
||||
if (!props.node.connectionId || !props.node.database) return;
|
||||
installing.value = name;
|
||||
try {
|
||||
const sql = buildCreateExtensionSql(name, props.node.schema ?? null);
|
||||
await api.executeQuery(props.node.connectionId, props.node.database, sql, props.node.schema ?? undefined);
|
||||
await loadData();
|
||||
emit("close");
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
|
||||
} finally {
|
||||
installing.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function dropExtension(name: string) {
|
||||
if (!props.node.connectionId || !props.node.database) return;
|
||||
dropping.value = name;
|
||||
try {
|
||||
const sql = buildDropExtensionSql(name, false);
|
||||
await api.executeQuery(props.node.connectionId, props.node.database, sql, props.node.schema ?? undefined);
|
||||
await loadData();
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
|
||||
} finally {
|
||||
dropping.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("extension.manageTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-2 gap-4">
|
||||
<!-- Left: Available -->
|
||||
<div class="flex flex-col min-h-[300px]">
|
||||
<div class="flex items-center gap-1.5 mb-2 text-sm font-medium text-muted-foreground">
|
||||
<Package class="h-4 w-4" />
|
||||
{{ t("extension.available") }}
|
||||
<span class="ml-auto text-xs">({{ available.length }})</span>
|
||||
</div>
|
||||
<ScrollArea class="flex-1 rounded-md border">
|
||||
<div v-if="available.length === 0" class="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
{{ t("extension.noAvailable") }}
|
||||
</div>
|
||||
<div v-else class="divide-y">
|
||||
<div v-for="ext in available" :key="ext.name" class="flex items-center justify-between gap-2 px-3 py-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium truncate">{{ ext.name }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ ext.comment || ext.version }}</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" :disabled="installing === ext.name" @click="installExtension(ext.name)">
|
||||
<Loader2 v-if="installing === ext.name" class="mr-1 h-3 w-3 animate-spin" />
|
||||
<Plus v-else class="mr-1 h-3 w-3" />
|
||||
{{ t("extension.install") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<!-- Right: Installed -->
|
||||
<div class="flex flex-col min-h-[300px]">
|
||||
<div class="flex items-center gap-1.5 mb-2 text-sm font-medium text-muted-foreground">
|
||||
<Package class="h-4 w-4" />
|
||||
{{ t("extension.installed") }}
|
||||
<span class="ml-auto text-xs">({{ installed.length }})</span>
|
||||
</div>
|
||||
<ScrollArea class="flex-1 rounded-md border">
|
||||
<div v-if="installed.length === 0" class="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
{{ t("extension.noInstalled") }}
|
||||
</div>
|
||||
<div v-else class="divide-y">
|
||||
<div v-for="ext in installed" :key="ext.name" class="flex items-center justify-between gap-2 px-3 py-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-medium truncate">{{ ext.name }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ ext.version }}{{ ext.comment ? ` — ${ext.comment}` : "" }}</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" :disabled="dropping === ext.name" @click="dropExtension(ext.name)">
|
||||
<Loader2 v-if="dropping === ext.name" class="mr-1 h-3 w-3 animate-spin" />
|
||||
<Trash2 v-else class="mr-1 h-3 w-3" />
|
||||
{{ t("extension.drop") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="open = false">{{ t("common.close") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in new issue
Block a user