feat: add update checking functionality and UI support

This commit is contained in:
t8y2
2026-04-30 16:09:44 +08:00
parent 4d8ae1221e
commit 4a72366863
7 changed files with 212 additions and 1 deletions
+78 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { useI18n } from "vue-i18n";
import { DatabaseZap, FilePlus2, Play, Loader2, X, Globe, Moon, Sun, Upload, Download, Plus, History, Server, Table2, Database, Search, ShieldCheck, Sparkles, Pin, AlignLeft } from "lucide-vue-next";
import { DatabaseZap, FilePlus2, Play, Loader2, X, Globe, Moon, Sun, Upload, Download, Plus, History, Server, Table2, Database, Search, ShieldCheck, Sparkles, Pin, AlignLeft, CloudDownload } from "lucide-vue-next";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";
@@ -12,6 +12,9 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle,
} from "@/components/ui/dialog";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import ConnectionTree from "@/components/sidebar/ConnectionTree.vue";
import ConnectionDialog from "@/components/connection/ConnectionDialog.vue";
@@ -45,6 +48,7 @@ const { message: toastMessage, visible: toastVisible, toast } = useToast();
const showConnectionDialog = ref(false);
const showHistory = ref(false);
const showUpdateDialog = ref(false);
const dangerSql = ref("");
const pendingDangerSql = ref("");
const selectedSql = ref("");
@@ -52,6 +56,10 @@ const formatSqlRequestId = ref(0);
const showDangerDialog = ref(false);
const databaseOptions = ref<Record<string, string[]>>({});
const loadingDatabaseOptions = ref<Record<string, boolean>>({});
const checkingUpdates = ref(false);
const updateInfo = ref<api.UpdateInfo | null>(null);
const updateCheckMessage = ref("");
const latestReleaseUrl = "https://github.com/t8y2/dbx/releases/latest";
const editConfig = computed(() => {
const id = connectionStore.editingConnectionId;
@@ -445,6 +453,42 @@ function openGitHub() {
open("https://github.com/t8y2/dbx");
}
async function checkUpdates(options: { silent?: boolean } = {}) {
if (checkingUpdates.value) return;
checkingUpdates.value = true;
updateCheckMessage.value = "";
try {
const info = await api.checkForUpdates();
updateInfo.value = info;
if (info.update_available) {
showUpdateDialog.value = true;
} else if (!options.silent) {
updateCheckMessage.value = t("updates.upToDate", { version: info.current_version });
showUpdateDialog.value = true;
}
} catch (e: any) {
if (!options.silent) {
updateCheckMessage.value = formatUpdateError(String(e));
showUpdateDialog.value = true;
}
} finally {
checkingUpdates.value = false;
}
}
function formatUpdateError(message: string): string {
const lower = message.toLowerCase();
if (lower.includes("403") || lower.includes("rate limit")) {
return t("updates.rateLimited");
}
return t("updates.failed", { error: message });
}
function openLatestRelease() {
const url = updateInfo.value?.release_url || latestReleaseUrl;
open(url);
}
function handleKeydown(e: KeyboardEvent) {
if (e.metaKey && e.key === "w") {
e.preventDefault();
@@ -460,6 +504,7 @@ onMounted(() => {
settingsStore.initAiConfig();
window.addEventListener("keydown", handleKeydown);
setupFileDrop();
checkUpdates({ silent: true });
});
onUnmounted(() => {
@@ -559,6 +604,16 @@ async function setupFileDrop() {
<div class="flex-1" />
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-7 w-7" :disabled="checkingUpdates" @click="checkUpdates()">
<Loader2 v-if="checkingUpdates" class="h-4 w-4 animate-spin" />
<CloudDownload v-else class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t('updates.check') }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-7 w-7" :class="{ 'bg-accent': showHistory }" @click="showHistory = !showHistory">
@@ -900,6 +955,28 @@ async function setupFileDrop() {
<ConnectionDialog v-model:open="showConnectionDialog" :edit-config="editConfig" />
<DangerConfirmDialog v-model:open="showDangerDialog" :sql="dangerSql" @confirm="onDangerConfirm" />
<Dialog v-model:open="showUpdateDialog">
<DialogContent class="sm:max-w-[520px]">
<DialogHeader>
<DialogTitle>{{ updateInfo?.update_available ? t('updates.availableTitle') : t('updates.title') }}</DialogTitle>
</DialogHeader>
<div class="space-y-3 text-sm">
<p v-if="updateInfo?.update_available">
{{ t('updates.availableMessage', { current: updateInfo.current_version, latest: updateInfo.latest_version }) }}
</p>
<p v-else class="text-muted-foreground">
{{ updateCheckMessage || t('updates.upToDate', { version: updateInfo?.current_version || '' }) }}
</p>
<div v-if="updateInfo?.update_available && updateInfo.release_notes" class="max-h-48 overflow-auto rounded-md border bg-muted/30 p-3 text-xs whitespace-pre-wrap">
{{ updateInfo.release_notes }}
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showUpdateDialog = false">{{ t('dangerDialog.cancel') }}</Button>
<Button v-if="updateInfo?.update_available || updateCheckMessage" @click="openLatestRelease">{{ t('updates.openRelease') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Global Toast -->
<Transition name="toast">
+10
View File
@@ -10,6 +10,16 @@ export default {
formatSql: "Format SQL",
formatSqlFailed: "Failed to format SQL",
},
updates: {
title: "Updates",
check: "Check for updates",
availableTitle: "Update available",
availableMessage: "DBX {latest} is available. You are using {current}.",
upToDate: "DBX is up to date ({version}).",
failed: "Failed to check updates: {error}",
rateLimited: "GitHub update checks are temporarily rate limited. You can still open the release page to check manually.",
openRelease: "Open Release",
},
sidebar: {
connections: "CONNECTIONS",
noConnections: "No connections yet",
+10
View File
@@ -10,6 +10,16 @@ export default {
formatSql: "格式化 SQL",
formatSqlFailed: "SQL 格式化失败",
},
updates: {
title: "更新",
check: "检查更新",
availableTitle: "发现新版本",
availableMessage: "DBX {latest} 已发布,当前版本为 {current}。",
upToDate: "DBX 已是最新版本 ({version})。",
failed: "检查更新失败:{error}",
rateLimited: "GitHub 更新检查暂时触发频率限制。你仍然可以打开下载页手动查看。",
openRelease: "打开下载页",
},
sidebar: {
connections: "连接",
noConnections: "暂无连接",
+14
View File
@@ -101,6 +101,20 @@ export async function loadConnections(): Promise<ConnectionConfig[]> {
return invoke("load_connections");
}
// --- Updates ---
export interface UpdateInfo {
current_version: string;
latest_version: string;
update_available: boolean;
release_name: string;
release_url: string;
release_notes: string;
}
export async function checkForUpdates(): Promise<UpdateInfo> {
return invoke("check_for_updates");
}
// --- Redis ---
export interface RedisKeyInfo {
key: string;