feat(mongodb): support direct URL connection string (#32)

* chore: bump version to 0.2.1

* fix(ci): wait for Release workflow before publishing packages

Changed trigger from release:published to workflow_run on Release
completion, so assets are already uploaded when download starts.

* feat(mongodb): support direct URL connection string

Allow pasting a full MongoDB URI (e.g. mongodb+srv://) to connect
to Atlas clusters, replica sets, and other multi-instance setups.
This commit is contained in:
skyler 2026-04-30 17:23:50 +08:00 committed by GitHub
parent 6b97f280ec
commit 2b4cd311b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 2 deletions

View File

@ -33,6 +33,8 @@ pub struct ConnectionConfig {
pub ssh_key_path: String,
#[serde(default)]
pub ssl: bool,
#[serde(default)]
pub connection_string: Option<String>,
}
fn default_ssh_port() -> u16 {
@ -98,7 +100,12 @@ impl ConnectionConfig {
"server=tcp:{host},{port};database={}",
self.database.as_deref().unwrap_or("master")
),
DatabaseType::MongoDb => format!("mongodb://{host}:{port}{db_part}"),
DatabaseType::MongoDb => {
if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) {
return cs.to_string();
}
format!("mongodb://{host}:{port}{db_part}")
}
DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"),
}
}
@ -151,6 +158,9 @@ impl ConnectionConfig {
self.database.as_deref().unwrap_or("master")
),
DatabaseType::MongoDb => {
if let Some(cs) = self.connection_string.as_deref().filter(|s| !s.is_empty()) {
return cs.to_string();
}
if self.username.is_empty() {
format!("mongodb://{host}:{port}{db_part}")
} else {
@ -210,6 +220,7 @@ mod tests {
ssh_password: String::new(),
ssh_key_path: String::new(),
ssl: false,
connection_string: None,
}
}

View File

@ -54,11 +54,13 @@ const defaultForm = (): Omit<ConnectionConfig, "id"> => ({
ssh_password: "",
ssh_key_path: "",
ssl: false,
connection_string: undefined,
});
const form = ref(defaultForm());
const selectedType = ref("mysql");
const customDriverName = ref("");
const mongoUseUrl = ref(false);
const colorOptions = [
{ value: "", class: "bg-transparent border-dashed", labelKey: "connection.colorNone" },
@ -146,8 +148,10 @@ watch(() => props.editConfig, (config) => {
ssh_password: config.ssh_password || "",
ssh_key_path: config.ssh_key_path || "",
ssl: config.ssl || false,
connection_string: config.connection_string,
};
selectedType.value = profile;
mongoUseUrl.value = !!config.connection_string;
customDriverName.value = isCustomCompatibleProfile() ? (config.driver_label || "") : "";
} else {
editingId.value = null;
@ -227,6 +231,7 @@ function resetForm() {
editingId.value = null;
form.value = defaultForm();
selectedType.value = "mysql";
mongoUseUrl.value = false;
testResult.value = null;
}
@ -377,6 +382,46 @@ watch([() => editingId.value, () => open.value], () => {
</div>
</template>
<!-- MongoDB: URL or form -->
<template v-else-if="form.db_type === 'mongodb'">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right text-xs">{{ t('connection.mode') }}</Label>
<div class="col-span-3 flex gap-2">
<Button size="sm" :variant="mongoUseUrl ? 'outline' : 'default'" @click="mongoUseUrl = false">{{ t('connection.modeForm') }}</Button>
<Button size="sm" :variant="mongoUseUrl ? 'default' : 'outline'" @click="mongoUseUrl = true">URL</Button>
</div>
</div>
<template v-if="mongoUseUrl">
<div class="grid grid-cols-4 items-start gap-4">
<Label class="text-right mt-2">URL</Label>
<textarea
v-model="form.connection_string"
class="col-span-3 flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
placeholder="mongodb+srv://user:pass@cluster.mongodb.net/mydb"
/>
</div>
</template>
<template v-else>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t('connection.host') }}</Label>
<Input v-model="form.host" class="col-span-2" />
<Input v-model.number="form.port" type="number" class="col-span-1" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t('connection.user') }}</Label>
<Input v-model="form.username" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t('connection.password') }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t('connection.database') }}</Label>
<Input v-model="form.database" class="col-span-3" :placeholder="t('connection.databasePlaceholder')" />
</div>
</template>
</template>
<!-- MySQL / PostgreSQL: host, port, user, password, database -->
<template v-else>
<div class="grid grid-cols-4 items-center gap-4">
@ -444,7 +489,7 @@ watch([() => editingId.value, () => open.value], () => {
<Button variant="outline" :disabled="isTesting || isSaving" @click="testConnection">
{{ isTesting ? t('connection.testing') : t('connection.test') }}
</Button>
<Button @click="save" :disabled="isSaving || !form.name || !form.host">
<Button @click="save" :disabled="isSaving || !form.name || (!form.host && !(mongoUseUrl && form.connection_string))">
{{ isSaving ? t('common.loading') : (editingId ? t('connection.save') : t('connection.saveAndConnect')) }}
</Button>
</DialogFooter>

View File

@ -41,6 +41,8 @@ export default {
driverName: "驱动名称",
driverNamePlaceholder: "厂商或环境名称",
urlParams: "URL 参数",
mode: "连接方式",
modeForm: "表单",
test: "测试",
testing: "测试中...",
saveAndConnect: "保存并连接",

View File

@ -20,6 +20,7 @@ export interface ConnectionConfig {
ssh_password?: string;
ssh_key_path?: string;
ssl?: boolean;
connection_string?: string;
}
export interface DatabaseInfo {