feat: optimize connection error alerts

This commit is contained in:
Wu Clan
2026-05-05 17:07:58 +08:00
parent 3acd9280ac
commit cc70e4b59c
16 changed files with 352 additions and 12 deletions
@@ -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>
+8 -1
View File
@@ -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) {
+13 -3
View File
@@ -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"
+19
View File
@@ -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>
+7
View File
@@ -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'