feat: optimize connection error alerts
This commit is contained in:
parent
3acd9280ac
commit
cc70e4b59c
|
|
@ -681,48 +681,56 @@ packages:
|
|||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-IxtQC/sbBi4ubbY+MdwdanRWrG9InQJVZqyMsBa5IUaQcnSg86gQme574HxXMC1p4bo4YhV99zQ+wNnGCvEgzw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-EWXEhOMbWO0q6eJSbu0QLkU8cKi0ljlYLngeDs2Ocu/pm1rrLwyQiYzlFbdnMRURI4w9ndr1sI9rSbhlJ5o23Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-tZrjS11TUiDuEpRaqdk8K9F9xETRyKXfuZKmdeW+Gj7coBnm7+8sBEfyt033EAFEQSlkniAXvBLh+Qja2ioGBQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-KBFy+2CFKUCZzYwX2ZOPQKck1vjQbz+hextuc19G4r0WRJwadfAeuQMQRQvB+Ivc8brlbOVg7et8K7E467440g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-REUPFKVGSiK99B+9eaPhluEVglzaoj/SMykNC5SUiV2RSsBfV5lWN7Y0iCIc251Wz3GaeAGZsJ/zj3gjarxdFg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.47.0':
|
||||
resolution: {integrity: sha512-KVftVSVEDeIfRW3TIeLe3aNI/iY4m1fu5mDwHcisKMZSCMKLkrhFsjowC7o9RoqNPxbbglm2+/6KAKBIts2t0Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.47.0':
|
||||
resolution: {integrity: sha512-DTsmGEaA2860Aq5VUyDO8/MT9NFxwVL93RnRYmpMwK6DsSkThmvEpqoUDDljziEpAedMRG19SCogrNbINSbLUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.47.0':
|
||||
resolution: {integrity: sha512-8r5BDro7fLOBoq1JXHLVSs55OlrxQhEso4HVo0TcY7OXJUPYfjPoOaYL5us+yIwqyP9rQwN+rxuiNFSmaxSuOQ==}
|
||||
|
|
@ -782,66 +790,79 @@ packages:
|
|||
resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.2':
|
||||
resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.2':
|
||||
resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.2':
|
||||
resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.2':
|
||||
resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.2':
|
||||
resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.2':
|
||||
resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.2':
|
||||
resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.2':
|
||||
resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.2':
|
||||
resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.2':
|
||||
resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.2':
|
||||
resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.2':
|
||||
resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.2':
|
||||
resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==}
|
||||
|
|
@ -914,24 +935,28 @@ packages:
|
|||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.4':
|
||||
resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.4':
|
||||
resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.4':
|
||||
resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.4':
|
||||
resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==}
|
||||
|
|
@ -1000,30 +1025,35 @@ packages:
|
|||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.11.0':
|
||||
resolution: {integrity: sha512-DtSE8ZBlB9H+L+eHkfZ3myt00EVEyAB3e41juEHoE2qT88fgVlJvyrwa9SZYc/xTwCS9TnmK+R84tpg+ZsAg7Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.11.0':
|
||||
resolution: {integrity: sha512-5QdgS4LD+kntClI1aj2JmwjW38LosNXxwCe8viIHEwqYIWuMPdNEIau6/cLogI38Yzx9DnfCPRfEWLyI+5li8Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.11.0':
|
||||
resolution: {integrity: sha512-5UynPXo3Zq9khjVdAbD+YogeLltdVUeOah2ioSIM3tu6H7wY9vMy6rgGJhv9r5R8ZXmk9GttMippdqYJWrnLnA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.11.0':
|
||||
resolution: {integrity: sha512-CNz7fHbApz1Zyhhq73jtGn9JqgNEV/lIWnTnUo6h6ujw+mHsTmkLszvJSM8W6JBaDjNpTTFr/RSNoVL5FMwcTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.11.0':
|
||||
resolution: {integrity: sha512-K+br+VXZ+Xx0n/9FdWohpW5Ugq+2FQUpJScqcPl1hTxXfh3fgjYgt4qA2NgrjlJo+zZPNrmUMl+NLvm0ufEqBQ==}
|
||||
|
|
@ -2068,24 +2098,28 @@ packages:
|
|||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
|
|
|
|||
20
src/App.vue
20
src/App.vue
|
|
@ -215,9 +215,15 @@ function newQuery() {
|
|||
async function openConnectionQuery(connectionId: string) {
|
||||
const connection = connectionStore.getConfig(connectionId);
|
||||
if (!connection) return;
|
||||
const options = await getDatabaseOptions(connectionId);
|
||||
connectionStore.activeConnectionId = connectionId;
|
||||
queryStore.createTab(connectionId, connection.database || options[0] || "");
|
||||
const tabId = queryStore.createTab(connectionId, connection.database || "");
|
||||
try {
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
const options = await getDatabaseOptions(connectionId);
|
||||
if (!connection.database && options[0]) queryStore.updateDatabase(tabId, options[0]);
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeActiveConnection(connectionId: string) {
|
||||
|
|
@ -225,9 +231,15 @@ async function changeActiveConnection(connectionId: string) {
|
|||
if (!tab) return;
|
||||
const connection = connectionStore.getConfig(connectionId);
|
||||
if (!connection) return;
|
||||
const options = await getDatabaseOptions(connectionId);
|
||||
queryStore.updateConnection(tab.id, connectionId, connection.database || options[0] || "");
|
||||
queryStore.updateConnection(tab.id, connectionId, connection.database || "");
|
||||
connectionStore.activeConnectionId = connectionId;
|
||||
try {
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
const options = await getDatabaseOptions(connectionId);
|
||||
if (!connection.database && options[0]) queryStore.updateDatabase(tab.id, options[0]);
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function changeActiveDatabase(database: string) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle, X } from "lucide-vue-next";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
connectionId?: string | null;
|
||||
triggerClass?: string;
|
||||
}>(),
|
||||
{
|
||||
connectionId: null,
|
||||
triggerClass: "",
|
||||
},
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const errorMessage = computed(() =>
|
||||
props.connectionId
|
||||
? connectionStore.connectionErrors[props.connectionId]
|
||||
: "",
|
||||
);
|
||||
|
||||
function clearError() {
|
||||
if (props.connectionId)
|
||||
connectionStore.clearConnectionError(props.connectionId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover v-if="errorMessage">
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded text-amber-500 hover:bg-amber-500/10 hover:text-amber-600 focus:outline-none focus:ring-1 focus:ring-amber-500/40"
|
||||
:class="triggerClass"
|
||||
:title="t('connection.lastError')"
|
||||
@click.stop
|
||||
>
|
||||
<AlertTriangle class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="top" class="w-72 gap-2 p-2 text-xs" @click.stop>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-medium text-foreground">
|
||||
{{ t("connection.lastError") }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 max-h-36 overflow-auto whitespace-pre-wrap break-words text-muted-foreground"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
:title="t('connection.clearError')"
|
||||
@click="clearError"
|
||||
>
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
|
|
@ -42,6 +42,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
|||
import { connectionIconType } from "@/lib/connectionPresentation";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { buildAiContext, runAiStream, type AiAction } from "@/lib/ai";
|
||||
import {
|
||||
aiTestConnection,
|
||||
|
|
@ -59,6 +60,7 @@ const { t } = useI18n();
|
|||
const settings = useSettingsStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
interface ChatMessage {
|
||||
role: "user" | "assistant";
|
||||
|
|
@ -133,7 +135,7 @@ async function loadDatabases() {
|
|||
await loadDatabaseOptions(props.connection.id);
|
||||
}
|
||||
|
||||
function changeConnection(connectionId: string) {
|
||||
async function changeConnection(connectionId: string) {
|
||||
const conn = connectionStore.getConfig(connectionId);
|
||||
if (!conn) return;
|
||||
connectionStore.activeConnectionId = connectionId;
|
||||
|
|
@ -143,6 +145,11 @@ function changeConnection(connectionId: string) {
|
|||
} else {
|
||||
queryStore.createTab(connectionId, conn.database || "");
|
||||
}
|
||||
try {
|
||||
await loadDatabaseOptions(connectionId);
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function changeDatabase(database: string) {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import { qualifiedTableName as buildQualifiedTableName, quoteTableIdentifier } f
|
|||
import { treeNodeRowAction } from "@/lib/treeNodeClick";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -304,9 +305,13 @@ async function openData() {
|
|||
async function newQuery() {
|
||||
const node = props.node;
|
||||
if (!node.connectionId) return;
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
queryStore.createTab(node.connectionId, node.database || "", undefined, "query");
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
queryStore.createTab(node.connectionId, node.database || "", undefined, "query");
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
|
|
@ -919,6 +924,11 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
|||
"
|
||||
class="w-1.5 h-1.5 rounded-full bg-green-500 shrink-0"
|
||||
/>
|
||||
<ConnectionErrorIndicator
|
||||
v-if="node.type === 'connection'"
|
||||
:connection-id="node.connectionId"
|
||||
trigger-class="h-4 w-4"
|
||||
/>
|
||||
<button
|
||||
v-if="canPin"
|
||||
class="rounded p-0.5 text-muted-foreground hover:bg-muted-foreground/15 hover:text-foreground focus:opacity-100"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui'
|
||||
import { PopoverRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="popover"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { PopoverAnchorProps } from 'reka-ui'
|
||||
import { PopoverAnchor } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverAnchorProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverAnchor
|
||||
data-slot="popover-anchor"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverAnchor>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
align: 'center',
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
data-slot="popover-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
data-slot="popover-description"
|
||||
:class="cn('text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="popover-header"
|
||||
:class="cn('flex flex-col gap-0.5 text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="popover-title"
|
||||
:class="cn('font-medium cn-font-heading', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import type { PopoverTriggerProps } from 'reka-ui'
|
||||
import { PopoverTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverTrigger
|
||||
data-slot="popover-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export { default as Popover } from './Popover.vue'
|
||||
export { default as PopoverAnchor } from './PopoverAnchor.vue'
|
||||
export { default as PopoverContent } from './PopoverContent.vue'
|
||||
export { default as PopoverDescription } from './PopoverDescription.vue'
|
||||
export { default as PopoverHeader } from './PopoverHeader.vue'
|
||||
export { default as PopoverTitle } from './PopoverTitle.vue'
|
||||
export { default as PopoverTrigger } from './PopoverTrigger.vue'
|
||||
|
|
@ -76,6 +76,8 @@ export default {
|
|||
connecting: "Connecting to {name}...",
|
||||
connectSuccess: "Connected to {name}",
|
||||
connectFailed: "Connection failed: {message}",
|
||||
lastError: "Connection error",
|
||||
clearError: "Clear connection error",
|
||||
saveFailed: "Failed to save connection: {message}",
|
||||
loadFailed: "Failed to load saved connections: {message}",
|
||||
sshTunnel: "SSH Tunnel",
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ export default {
|
|||
connecting: "正在连接 {name}...",
|
||||
connectSuccess: "已连接 {name}",
|
||||
connectFailed: "连接失败:{message}",
|
||||
lastError: "连接错误",
|
||||
clearError: "清除连接错误",
|
||||
saveFailed: "保存连接失败:{message}",
|
||||
loadFailed: "加载已保存连接失败:{message}",
|
||||
sshTunnel: "SSH 隧道",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const treeNodes = ref<TreeNode[]>([]);
|
||||
const pinnedTreeNodeIds = ref<Set<string>>(loadPinnedTreeNodeIds());
|
||||
const connectedIds = ref<Set<string>>(new Set());
|
||||
const connectionErrors = ref<Record<string, string>>({});
|
||||
const editingConnectionId = ref<string | null>(null);
|
||||
const completionTablesCache = ref<Record<string, SqlCompletionTable[]>>({});
|
||||
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
|
||||
|
|
@ -86,6 +87,31 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
return connections.value.find((c) => c.id === connectionId);
|
||||
}
|
||||
|
||||
function connectionErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function setConnectionError(connectionId: string, message: string) {
|
||||
connectionErrors.value = {
|
||||
...connectionErrors.value,
|
||||
[connectionId]: message,
|
||||
};
|
||||
}
|
||||
|
||||
function clearConnectionError(connectionId: string) {
|
||||
if (!connectionErrors.value[connectionId]) return;
|
||||
const next = { ...connectionErrors.value };
|
||||
delete next[connectionId];
|
||||
connectionErrors.value = next;
|
||||
}
|
||||
|
||||
function recordConnectionError(connectionId: string, error: unknown): string {
|
||||
const message = connectionErrorMessage(error);
|
||||
setConnectionError(connectionId, message);
|
||||
return message;
|
||||
}
|
||||
|
||||
function normalizeConnection(config: ConnectionConfig): ConnectionConfig {
|
||||
const labelMap: Record<string, string> = {
|
||||
mysql: "MySQL",
|
||||
|
|
@ -203,6 +229,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const nextConnections = connections.value.filter((c) => c.id !== id);
|
||||
await persistConnections(nextConnections);
|
||||
connections.value = nextConnections;
|
||||
clearConnectionError(id);
|
||||
sidebarLayout.value = removeConnectionFromSidebarLayout(sidebarLayout.value, id);
|
||||
rebuildTreeNodes();
|
||||
persistSidebarLayoutDebounced();
|
||||
|
|
@ -233,6 +260,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const id = await api.connectDb(config);
|
||||
activeConnectionId.value = id;
|
||||
connectedIds.value.add(id);
|
||||
clearConnectionError(config.id);
|
||||
if (id !== config.id) clearConnectionError(id);
|
||||
|
||||
const node: TreeNode = {
|
||||
id,
|
||||
|
|
@ -249,6 +278,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
treeNodes.value.push(node);
|
||||
}
|
||||
return id;
|
||||
} catch (e) {
|
||||
recordConnectionError(config.id, e);
|
||||
throw e;
|
||||
} finally {
|
||||
const node = findNode(treeNodes.value, config.id);
|
||||
if (node) node.isLoading = false;
|
||||
|
|
@ -272,10 +304,20 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
async function ensureConnected(connectionId: string) {
|
||||
if (connectedIds.value.has(connectionId)) return;
|
||||
const config = getConfig(connectionId);
|
||||
if (!config) throw new Error("Connection config not found");
|
||||
await api.connectDb(config);
|
||||
connectedIds.value.add(connectionId);
|
||||
activeConnectionId.value = connectionId;
|
||||
if (!config) {
|
||||
const error = new Error("Connection config not found");
|
||||
recordConnectionError(connectionId, error);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
await api.connectDb(config);
|
||||
connectedIds.value.add(connectionId);
|
||||
activeConnectionId.value = connectionId;
|
||||
clearConnectionError(connectionId);
|
||||
} catch (e) {
|
||||
recordConnectionError(connectionId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDatabases(connectionId: string) {
|
||||
|
|
@ -858,6 +900,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
connections.value.push(normalized);
|
||||
}
|
||||
connectedIds.value.add(normalized.id);
|
||||
clearConnectionError(normalized.id);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -865,6 +908,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
activeConnectionId,
|
||||
treeNodes,
|
||||
connectedIds,
|
||||
connectionErrors,
|
||||
setConnectionError,
|
||||
clearConnectionError,
|
||||
recordConnectionError,
|
||||
sidebarLayout,
|
||||
getConfig,
|
||||
isTreeNodePinned,
|
||||
|
|
|
|||
Loading…
Reference in New Issue