Add update checks and improve SSH tunnel handling
Merge update checks and SSH tunnel connection handling improvements.
This commit is contained in:
commit
da154c0dc2
|
|
@ -79,7 +79,8 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
let url = db_config.connection_url();
|
||||
let (host, port) = self.connection_host_port(connection_id, &db_config).await?;
|
||||
let url = connection_url_for_endpoint(&db_config, &host, port);
|
||||
let pool = match db_config.db_type {
|
||||
DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?),
|
||||
DatabaseType::Postgres => PoolKind::Postgres(db::postgres::connect(&url).await?),
|
||||
|
|
@ -103,18 +104,23 @@ impl AppState {
|
|||
}
|
||||
DatabaseType::SqlServer => {
|
||||
let client = db::sqlserver::connect(
|
||||
&db_config.host, db_config.port,
|
||||
&db_config.username, &db_config.password,
|
||||
&host,
|
||||
port,
|
||||
&db_config.username,
|
||||
&db_config.password,
|
||||
db_config.database.as_deref(),
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
DatabaseType::Oracle => {
|
||||
let client = db::oracle_driver::connect(
|
||||
&db_config.host, db_config.port,
|
||||
&host,
|
||||
port,
|
||||
db_config.database.as_deref().unwrap_or("ORCL"),
|
||||
&db_config.username, &db_config.password,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
PoolKind::Oracle(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
};
|
||||
|
|
@ -123,6 +129,36 @@ impl AppState {
|
|||
Ok(pool_key)
|
||||
}
|
||||
|
||||
async fn connection_host_port(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
config: &ConnectionConfig,
|
||||
) -> Result<(String, u16), String> {
|
||||
if !config.ssh_enabled || config.ssh_host.is_empty() {
|
||||
return Ok((config.host.clone(), config.port));
|
||||
}
|
||||
|
||||
if let Some(local_port) = self.tunnels.local_port(connection_id).await {
|
||||
return Ok(("127.0.0.1".to_string(), local_port));
|
||||
}
|
||||
|
||||
let local_port = self
|
||||
.tunnels
|
||||
.start_tunnel(
|
||||
connection_id,
|
||||
&config.ssh_host,
|
||||
config.ssh_port,
|
||||
&config.ssh_user,
|
||||
&config.ssh_password,
|
||||
&config.ssh_key_path,
|
||||
&config.host,
|
||||
config.port,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(("127.0.0.1".to_string(), local_port))
|
||||
}
|
||||
|
||||
pub async fn reconnect_pool(
|
||||
&self,
|
||||
connection_id: &str,
|
||||
|
|
@ -153,6 +189,26 @@ fn connections_file(app: &AppHandle) -> Result<std::path::PathBuf, String> {
|
|||
Ok(dir.join("connections.json"))
|
||||
}
|
||||
|
||||
fn connection_url_for_endpoint(config: &ConnectionConfig, host: &str, port: u16) -> String {
|
||||
if host == config.host && port == config.port {
|
||||
config.connection_url()
|
||||
} else {
|
||||
config.connection_url_with_host(host, port)
|
||||
}
|
||||
}
|
||||
|
||||
fn redacted_connection_url_for_endpoint(
|
||||
config: &ConnectionConfig,
|
||||
host: &str,
|
||||
port: u16,
|
||||
) -> String {
|
||||
if host == config.host && port == config.port {
|
||||
config.redacted_connection_url()
|
||||
} else {
|
||||
config.redacted_connection_url_with_host(host, port)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_connections(
|
||||
app: AppHandle,
|
||||
|
|
@ -177,64 +233,95 @@ pub async fn load_connections(app: AppHandle) -> Result<Vec<ConnectionConfig>, S
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_connection(config: ConnectionConfig) -> Result<String, String> {
|
||||
let url = config.connection_url();
|
||||
pub async fn test_connection(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
config: ConnectionConfig,
|
||||
) -> Result<String, String> {
|
||||
let tunnel_id = format!("{}:test", config.id);
|
||||
let connection_id = if config.ssh_enabled && !config.ssh_host.is_empty() {
|
||||
tunnel_id.as_str()
|
||||
} else {
|
||||
config.id.as_str()
|
||||
};
|
||||
let (host, port) = state.connection_host_port(connection_id, &config).await?;
|
||||
let url = connection_url_for_endpoint(&config, &host, port);
|
||||
let target = redacted_connection_url_for_endpoint(&config, &host, port);
|
||||
log::info!(
|
||||
"[test_connection] db_type={:?} target={}",
|
||||
config.db_type,
|
||||
config.redacted_connection_url()
|
||||
target
|
||||
);
|
||||
match config.db_type {
|
||||
DatabaseType::Mysql => {
|
||||
let pool = db::mysql::connect(&url).await?;
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::Postgres => {
|
||||
let pool = db::postgres::connect(&url).await?;
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::Sqlite => {
|
||||
let pool = db::sqlite::connect(&url).await?;
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
let result = match config.db_type {
|
||||
DatabaseType::Mysql => match db::mysql::connect(&url).await {
|
||||
Ok(pool) => {
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
DatabaseType::Postgres => match db::postgres::connect(&url).await {
|
||||
Ok(pool) => {
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
DatabaseType::Sqlite => match db::sqlite::connect(&url).await {
|
||||
Ok(pool) => {
|
||||
pool.close().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
DatabaseType::Redis => {
|
||||
let _con = db::redis_driver::connect(&url).await?;
|
||||
Ok("Connection successful".to_string())
|
||||
db::redis_driver::connect(&url)
|
||||
.await
|
||||
.map(|_| "Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::DuckDb => {
|
||||
let _con = duckdb::Connection::open(&config.host).map_err(|e| e.to_string())?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::MongoDb => {
|
||||
let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?;
|
||||
client.list_database_names().await.map_err(|e| e.to_string())?;
|
||||
Ok("Connection successful".to_string())
|
||||
duckdb::Connection::open(&config.host)
|
||||
.map(|_| "Connection successful".to_string())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
DatabaseType::MongoDb => match mongodb::Client::with_uri_str(&url).await {
|
||||
Ok(client) => client
|
||||
.list_database_names()
|
||||
.await
|
||||
.map(|_| "Connection successful".to_string())
|
||||
.map_err(|e| e.to_string()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
},
|
||||
DatabaseType::ClickHouse => {
|
||||
let client = db::clickhouse_driver::ChClient::new(&url);
|
||||
db::clickhouse_driver::test_connection(&client).await?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::SqlServer => {
|
||||
let _client = db::sqlserver::connect(
|
||||
&config.host, config.port,
|
||||
&config.username, &config.password,
|
||||
config.database.as_deref(),
|
||||
).await?;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::Oracle => {
|
||||
let _client = db::oracle_driver::connect(
|
||||
&config.host, config.port,
|
||||
config.database.as_deref().unwrap_or("ORCL"),
|
||||
&config.username, &config.password,
|
||||
).await?;
|
||||
Ok("Connection successful".to_string())
|
||||
db::clickhouse_driver::test_connection(&client)
|
||||
.await
|
||||
.map(|_| "Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::SqlServer => db::sqlserver::connect(
|
||||
&host,
|
||||
port,
|
||||
&config.username,
|
||||
&config.password,
|
||||
config.database.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map(|_| "Connection successful".to_string()),
|
||||
DatabaseType::Oracle => db::oracle_driver::connect(
|
||||
&host,
|
||||
port,
|
||||
config.database.as_deref().unwrap_or("ORCL"),
|
||||
&config.username,
|
||||
&config.password,
|
||||
)
|
||||
.await
|
||||
.map(|_| "Connection successful".to_string()),
|
||||
};
|
||||
|
||||
if config.ssh_enabled && !config.ssh_host.is_empty() {
|
||||
state.tunnels.stop_tunnel(&tunnel_id).await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -244,16 +331,8 @@ pub async fn connect_db(
|
|||
) -> Result<String, String> {
|
||||
let id = config.id.clone();
|
||||
|
||||
let url = if config.ssh_enabled && !config.ssh_host.is_empty() {
|
||||
let local_port = state.tunnels.start_tunnel(
|
||||
&id, &config.ssh_host, config.ssh_port,
|
||||
&config.ssh_user, &config.ssh_password, &config.ssh_key_path,
|
||||
&config.host, config.port,
|
||||
).await?;
|
||||
config.connection_url_with_host("127.0.0.1", local_port)
|
||||
} else {
|
||||
config.connection_url()
|
||||
};
|
||||
let (host, port) = state.connection_host_port(&id, &config).await?;
|
||||
let url = connection_url_for_endpoint(&config, &host, port);
|
||||
|
||||
let pool = match config.db_type {
|
||||
DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?),
|
||||
|
|
@ -278,17 +357,23 @@ pub async fn connect_db(
|
|||
}
|
||||
DatabaseType::SqlServer => {
|
||||
let client = db::sqlserver::connect(
|
||||
&config.host, config.port,
|
||||
&host,
|
||||
port,
|
||||
&config.username, &config.password,
|
||||
config.database.as_deref(),
|
||||
).await?;
|
||||
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client))) }
|
||||
)
|
||||
.await?;
|
||||
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
DatabaseType::Oracle => {
|
||||
let client = db::oracle_driver::connect(
|
||||
&config.host, config.port,
|
||||
&host,
|
||||
port,
|
||||
config.database.as_deref().unwrap_or("ORCL"),
|
||||
&config.username, &config.password,
|
||||
).await?;
|
||||
&config.username,
|
||||
&config.password,
|
||||
)
|
||||
.await?;
|
||||
PoolKind::Oracle(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ pub mod mongo_cmd;
|
|||
pub mod query;
|
||||
pub mod redis_cmd;
|
||||
pub mod schema;
|
||||
pub mod update;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/t8y2/dbx/releases/latest";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GithubRelease {
|
||||
tag_name: String,
|
||||
name: Option<String>,
|
||||
html_url: String,
|
||||
body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub current_version: String,
|
||||
pub latest_version: String,
|
||||
pub update_available: bool,
|
||||
pub release_name: String,
|
||||
pub release_url: String,
|
||||
pub release_notes: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates() -> Result<UpdateInfo, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let release = client
|
||||
.get(LATEST_RELEASE_URL)
|
||||
.header(reqwest::header::USER_AGENT, "dbx-update-checker")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to check updates: {e}"))?
|
||||
.error_for_status()
|
||||
.map_err(|e| format!("Failed to check updates: {e}"))?
|
||||
.json::<GithubRelease>()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse update response: {e}"))?;
|
||||
|
||||
let current_version = env!("CARGO_PKG_VERSION").to_string();
|
||||
let latest_version = normalize_version(&release.tag_name);
|
||||
|
||||
Ok(UpdateInfo {
|
||||
update_available: is_newer_version(&latest_version, ¤t_version),
|
||||
current_version,
|
||||
latest_version,
|
||||
release_name: release.name.unwrap_or_else(|| release.tag_name.clone()),
|
||||
release_url: release.html_url,
|
||||
release_notes: release.body.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_version(version: &str) -> String {
|
||||
version.trim().trim_start_matches('v').to_string()
|
||||
}
|
||||
|
||||
fn parse_version(version: &str) -> Vec<u64> {
|
||||
normalize_version(version)
|
||||
.split(['.', '-', '+'])
|
||||
.map(|part| part.parse::<u64>().unwrap_or(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_newer_version(latest: &str, current: &str) -> bool {
|
||||
let latest_parts = parse_version(latest);
|
||||
let current_parts = parse_version(current);
|
||||
let max_len = latest_parts.len().max(current_parts.len());
|
||||
|
||||
for i in 0..max_len {
|
||||
let latest_part = *latest_parts.get(i).unwrap_or(&0);
|
||||
let current_part = *current_parts.get(i).unwrap_or(&0);
|
||||
if latest_part > current_part {
|
||||
return true;
|
||||
}
|
||||
if latest_part < current_part {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{is_newer_version, normalize_version};
|
||||
|
||||
#[test]
|
||||
fn normalizes_tag_versions() {
|
||||
assert_eq!(normalize_version("v1.2.3"), "1.2.3");
|
||||
assert_eq!(normalize_version(" 0.2.0 "), "0.2.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compares_semver_like_versions() {
|
||||
assert!(is_newer_version("0.2.1", "0.2.0"));
|
||||
assert!(is_newer_version("1.0.0", "0.9.9"));
|
||||
assert!(!is_newer_version("0.2.0", "0.2.0"));
|
||||
assert!(!is_newer_version("0.1.9", "0.2.0"));
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,14 @@ impl TunnelManager {
|
|||
Ok(local_port)
|
||||
}
|
||||
|
||||
pub async fn local_port(&self, connection_id: &str) -> Option<u16> {
|
||||
self.tunnels
|
||||
.lock()
|
||||
.await
|
||||
.get(connection_id)
|
||||
.map(|(_, port)| *port)
|
||||
}
|
||||
|
||||
pub async fn stop_tunnel(&self, connection_id: &str) {
|
||||
if let Some((mut child, _)) = self.tunnels.lock().await.remove(connection_id) {
|
||||
let _ = child.kill().await;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ pub fn run() {
|
|||
commands::history::load_history,
|
||||
commands::history::clear_history,
|
||||
commands::history::delete_history_entry,
|
||||
commands::update::check_for_updates,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
|||
79
src/App.vue
79
src/App.vue
|
|
@ -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,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,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: "暂无连接",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue