All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m3s
Root cause of pubkey auth failure: SSH key credentials had no username, so ConnectSSH defaulted to "root" and the server rejected the key. The SSH key form in ConnectionEditDialog only had Name, PEM, Passphrase. Added Username field between Name and PEM. Delete your existing SSH key credentials and re-create them with the correct username. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
597 lines
23 KiB
Vue
597 lines
23 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="visible"
|
|
class="fixed inset-0 z-50 flex items-center justify-center"
|
|
@click.self="close"
|
|
@keydown.esc="close"
|
|
>
|
|
<!-- Backdrop -->
|
|
<div class="absolute inset-0 bg-black/50" @click="close" />
|
|
|
|
<!-- Dialog -->
|
|
<div class="relative w-full max-w-md bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
|
|
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">
|
|
{{ isEditing ? "Edit Connection" : "New Connection" }}
|
|
</h3>
|
|
<button
|
|
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
|
@click="close"
|
|
>
|
|
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.749.749 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.749.749 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="px-4 py-4 space-y-3 max-h-[80vh] overflow-y-auto">
|
|
<!-- Name -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Name</label>
|
|
<input
|
|
v-model="form.name"
|
|
type="text"
|
|
placeholder="My Server"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Hostname & Port -->
|
|
<div class="flex gap-3">
|
|
<div class="flex-1">
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Hostname</label>
|
|
<input
|
|
v-model="form.hostname"
|
|
type="text"
|
|
placeholder="192.168.1.1"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
<div class="w-24">
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Port</label>
|
|
<input
|
|
v-model.number="form.port"
|
|
type="number"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Protocol -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Protocol</label>
|
|
<div class="flex gap-2">
|
|
<button
|
|
class="flex-1 py-2 text-sm rounded border transition-colors cursor-pointer"
|
|
:class="form.protocol === 'ssh'
|
|
? 'bg-[#3fb950]/10 border-[#3fb950] text-[#3fb950]'
|
|
: 'bg-[#0d1117] border-[#30363d] text-[var(--wraith-text-muted)] hover:border-[var(--wraith-text-secondary)]'
|
|
"
|
|
@click="setProtocol('ssh')"
|
|
>
|
|
SSH
|
|
</button>
|
|
<button
|
|
class="flex-1 py-2 text-sm rounded border transition-colors cursor-pointer"
|
|
:class="form.protocol === 'rdp'
|
|
? 'bg-[#1f6feb]/10 border-[#1f6feb] text-[#1f6feb]'
|
|
: 'bg-[#0d1117] border-[#30363d] text-[var(--wraith-text-muted)] hover:border-[var(--wraith-text-secondary)]'
|
|
"
|
|
@click="setProtocol('rdp')"
|
|
>
|
|
RDP
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Group -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Group</label>
|
|
<select
|
|
v-model="form.groupId"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
|
>
|
|
<option :value="null">No Group</option>
|
|
<option v-for="group in connectionStore.groups" :key="group.id" :value="group.id">
|
|
{{ group.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Tags (comma-separated)</label>
|
|
<input
|
|
v-model="tagsInput"
|
|
type="text"
|
|
placeholder="Prod, Linux, Web"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Color -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Color Label</label>
|
|
<div class="flex gap-2">
|
|
<button
|
|
v-for="color in colorOptions"
|
|
:key="color.value"
|
|
class="w-6 h-6 rounded-full border-2 transition-transform cursor-pointer hover:scale-110"
|
|
:class="form.color === color.value ? 'border-white scale-110' : 'border-transparent'"
|
|
:style="{ backgroundColor: color.hex }"
|
|
:title="color.label"
|
|
@click="form.color = color.value"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Notes</label>
|
|
<textarea
|
|
v-model="form.notes"
|
|
rows="3"
|
|
placeholder="Optional notes about this connection..."
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors resize-none"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="border-t border-[#30363d]" />
|
|
|
|
<!-- Credentials -->
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Credential</label>
|
|
<div class="flex gap-2">
|
|
<select
|
|
v-model="form.credentialId"
|
|
class="flex-1 px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
|
>
|
|
<option :value="null">None</option>
|
|
<option
|
|
v-for="cred in credentials"
|
|
:key="cred.id"
|
|
:value="cred.id"
|
|
>
|
|
{{ cred.name }}
|
|
<template v-if="cred.type === 'ssh_key'"> (SSH Key)</template>
|
|
<template v-else> (Password{{ cred.username ? ` — ${cred.username}` : '' }})</template>
|
|
</option>
|
|
</select>
|
|
<button
|
|
v-if="form.credentialId"
|
|
type="button"
|
|
class="px-2.5 py-2 text-xs rounded border border-red-500/30 text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer"
|
|
title="Delete selected credential"
|
|
@click="deleteSelectedCredential"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add New Credential (collapsible) -->
|
|
<div class="rounded border border-[#30363d] overflow-hidden">
|
|
<!-- Toggle header -->
|
|
<button
|
|
class="w-full flex items-center justify-between px-3 py-2 text-xs text-[var(--wraith-text-secondary)] hover:bg-[#1c2128] transition-colors cursor-pointer"
|
|
@click="showNewCred = !showNewCred"
|
|
>
|
|
<span class="font-medium">Add New Credential</span>
|
|
<svg
|
|
class="w-3.5 h-3.5 transition-transform"
|
|
:class="showNewCred ? 'rotate-180' : ''"
|
|
viewBox="0 0 16 16"
|
|
fill="currentColor"
|
|
>
|
|
<path d="M4.427 7.427l3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Collapsed content -->
|
|
<div v-if="showNewCred" class="px-3 pb-3 pt-2 space-y-3 bg-[#0d1117]">
|
|
<!-- Credential type selector -->
|
|
<div class="flex gap-2">
|
|
<button
|
|
class="flex-1 py-1.5 text-xs rounded border transition-colors cursor-pointer"
|
|
:class="newCredType === 'password'
|
|
? 'bg-[#3fb950]/10 border-[#3fb950] text-[#3fb950]'
|
|
: 'bg-transparent border-[#30363d] text-[var(--wraith-text-muted)] hover:border-[var(--wraith-text-secondary)]'
|
|
"
|
|
@click="newCredType = 'password'"
|
|
>
|
|
Password
|
|
</button>
|
|
<button
|
|
class="flex-1 py-1.5 text-xs rounded border transition-colors cursor-pointer"
|
|
:class="newCredType === 'ssh_key'
|
|
? 'bg-[#58a6ff]/10 border-[#58a6ff] text-[#58a6ff]'
|
|
: 'bg-transparent border-[#30363d] text-[var(--wraith-text-muted)] hover:border-[var(--wraith-text-secondary)]'
|
|
"
|
|
@click="newCredType = 'ssh_key'"
|
|
>
|
|
SSH Key
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Password fields -->
|
|
<template v-if="newCredType === 'password'">
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Credential Name</label>
|
|
<input
|
|
v-model="newCred.name"
|
|
type="text"
|
|
placeholder="e.g. prod-admin"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#161b22] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Username</label>
|
|
<input
|
|
v-model="newCred.username"
|
|
type="text"
|
|
placeholder="admin"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#161b22] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Password</label>
|
|
<input
|
|
v-model="newCred.password"
|
|
type="password"
|
|
placeholder="••••••••"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#161b22] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- SSH Key fields -->
|
|
<template v-if="newCredType === 'ssh_key'">
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Credential Name</label>
|
|
<input
|
|
v-model="newCred.name"
|
|
type="text"
|
|
placeholder="e.g. my-ssh-key"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#161b22] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Username</label>
|
|
<input
|
|
v-model="newCred.username"
|
|
type="text"
|
|
placeholder="e.g. vstockwell"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#161b22] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Private Key (PEM)</label>
|
|
<textarea
|
|
v-model="newCred.privateKeyPEM"
|
|
rows="5"
|
|
placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#161b22] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors resize-none font-mono"
|
|
spellcheck="false"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Passphrase <span class="text-[var(--wraith-text-muted)]">(optional)</span></label>
|
|
<input
|
|
v-model="newCred.passphrase"
|
|
type="password"
|
|
placeholder="Leave blank if key is unencrypted"
|
|
class="w-full px-3 py-2 text-sm rounded bg-[#161b22] border border-[#30363d] text-[var(--wraith-text-primary)] placeholder-[var(--wraith-text-muted)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Error message -->
|
|
<p v-if="newCredError" class="text-xs text-[#f85149]">{{ newCredError }}</p>
|
|
|
|
<!-- Save credential button -->
|
|
<button
|
|
class="w-full py-1.5 text-xs rounded border transition-colors cursor-pointer"
|
|
:class="isNewCredValid
|
|
? 'bg-[#238636] border-[#238636] text-white hover:bg-[#2ea043] hover:border-[#2ea043]'
|
|
: 'bg-transparent border-[#30363d] text-[var(--wraith-text-muted)] cursor-not-allowed opacity-50'
|
|
"
|
|
:disabled="!isNewCredValid || savingCred"
|
|
@click="saveNewCredential"
|
|
>
|
|
{{ savingCred ? 'Saving...' : 'Save Credential' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-end gap-2 px-4 py-3 border-t border-[#30363d]">
|
|
<button
|
|
class="px-3 py-1.5 text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
|
|
@click="close"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
class="px-3 py-1.5 text-xs text-white bg-[#238636] hover:bg-[#2ea043] rounded transition-colors cursor-pointer"
|
|
:class="{ 'opacity-50 cursor-not-allowed': !isValid }"
|
|
:disabled="!isValid"
|
|
@click="save"
|
|
>
|
|
{{ isEditing ? "Save Changes" : "Create" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from "vue";
|
|
import { useConnectionStore, type Connection } from "@/stores/connection.store";
|
|
import { Call } from "@wailsio/runtime";
|
|
|
|
/** Fully qualified Go method name prefix for ConnectionService bindings. */
|
|
const CONN = "github.com/vstockwell/wraith/internal/connections.ConnectionService";
|
|
/**
|
|
* Credential methods are proxied through WraithApp (registered at startup)
|
|
* because CredentialService is nil until vault unlock and can't be pre-registered.
|
|
*/
|
|
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
|
|
|
|
interface Credential {
|
|
id: number;
|
|
name: string;
|
|
username: string;
|
|
domain: string;
|
|
type: "password" | "ssh_key";
|
|
sshKeyId: number | null;
|
|
}
|
|
|
|
interface ConnectionForm {
|
|
name: string;
|
|
hostname: string;
|
|
port: number;
|
|
protocol: "ssh" | "rdp";
|
|
groupId: number | null;
|
|
credentialId: number | null;
|
|
color: string;
|
|
notes: string;
|
|
}
|
|
|
|
interface NewCredForm {
|
|
name: string;
|
|
username: string;
|
|
password: string;
|
|
privateKeyPEM: string;
|
|
passphrase: string;
|
|
}
|
|
|
|
const connectionStore = useConnectionStore();
|
|
|
|
const visible = ref(false);
|
|
const isEditing = ref(false);
|
|
const editingId = ref<number | null>(null);
|
|
const tagsInput = ref("");
|
|
|
|
const credentials = ref<Credential[]>([]);
|
|
|
|
// New credential panel state
|
|
const showNewCred = ref(false);
|
|
const newCredType = ref<"password" | "ssh_key">("password");
|
|
const savingCred = ref(false);
|
|
const newCredError = ref("");
|
|
const newCred = ref<NewCredForm>({
|
|
name: "",
|
|
username: "",
|
|
password: "",
|
|
privateKeyPEM: "",
|
|
passphrase: "",
|
|
});
|
|
|
|
const form = ref<ConnectionForm>({
|
|
name: "",
|
|
hostname: "",
|
|
port: 22,
|
|
protocol: "ssh",
|
|
groupId: null,
|
|
credentialId: null,
|
|
color: "",
|
|
notes: "",
|
|
});
|
|
|
|
const colorOptions = [
|
|
{ value: "", label: "None", hex: "#30363d" },
|
|
{ value: "red", label: "Red", hex: "#f85149" },
|
|
{ value: "orange", label: "Orange", hex: "#d29922" },
|
|
{ value: "green", label: "Green", hex: "#3fb950" },
|
|
{ value: "blue", label: "Blue", hex: "#58a6ff" },
|
|
{ value: "purple", label: "Purple", hex: "#bc8cff" },
|
|
{ value: "pink", label: "Pink", hex: "#f778ba" },
|
|
];
|
|
|
|
const isValid = computed(() => {
|
|
return form.value.name.trim() !== "" && form.value.hostname.trim() !== "" && form.value.port > 0;
|
|
});
|
|
|
|
const isNewCredValid = computed(() => {
|
|
if (!newCred.value.name.trim()) return false;
|
|
if (newCredType.value === "password") {
|
|
return newCred.value.password.length > 0;
|
|
}
|
|
// ssh_key: must have PEM content
|
|
return newCred.value.privateKeyPEM.trim().length > 0;
|
|
});
|
|
|
|
function setProtocol(protocol: "ssh" | "rdp"): void {
|
|
form.value.protocol = protocol;
|
|
if (protocol === "ssh" && form.value.port === 3389) {
|
|
form.value.port = 22;
|
|
} else if (protocol === "rdp" && form.value.port === 22) {
|
|
form.value.port = 3389;
|
|
}
|
|
}
|
|
|
|
function resetNewCredForm(): void {
|
|
newCred.value = { name: "", username: "", password: "", privateKeyPEM: "", passphrase: "" };
|
|
newCredError.value = "";
|
|
}
|
|
|
|
async function deleteSelectedCredential(): Promise<void> {
|
|
if (!form.value.credentialId) return;
|
|
const cred = credentials.value.find(c => c.id === form.value.credentialId);
|
|
const name = cred?.name ?? `ID ${form.value.credentialId}`;
|
|
if (!confirm(`Delete credential "${name}"? This cannot be undone.`)) return;
|
|
try {
|
|
await Call.ByName(`${APP}.DeleteCredential`, form.value.credentialId);
|
|
form.value.credentialId = null;
|
|
await loadCredentials();
|
|
} catch (err) {
|
|
alert(`Failed to delete credential: ${err}`);
|
|
}
|
|
}
|
|
|
|
async function loadCredentials(): Promise<void> {
|
|
try {
|
|
const result = await Call.ByName(`${APP}.ListCredentials`) as Credential[];
|
|
credentials.value = result || [];
|
|
} catch (err) {
|
|
credentials.value = [];
|
|
}
|
|
}
|
|
|
|
async function saveNewCredential(): Promise<void> {
|
|
if (!isNewCredValid.value || savingCred.value) return;
|
|
savingCred.value = true;
|
|
newCredError.value = "";
|
|
|
|
try {
|
|
let created: Credential | null = null;
|
|
|
|
if (newCredType.value === "password") {
|
|
created = await Call.ByName(
|
|
`${APP}.CreatePassword`,
|
|
newCred.value.name.trim(),
|
|
newCred.value.username.trim(),
|
|
newCred.value.password,
|
|
"", // domain — not collected in this form
|
|
) as Credential;
|
|
} else {
|
|
// SSH Key: CreateSSHKeyCredential(name, username, privateKeyPEM string, passphrase string)
|
|
created = await Call.ByName(
|
|
`${APP}.CreateSSHKeyCredential`,
|
|
newCred.value.name.trim(),
|
|
newCred.value.username.trim(),
|
|
newCred.value.privateKeyPEM.trim(),
|
|
newCred.value.passphrase,
|
|
) as Credential;
|
|
}
|
|
|
|
// Refresh list and auto-select the new credential
|
|
await loadCredentials();
|
|
if (created) {
|
|
form.value.credentialId = created.id;
|
|
}
|
|
|
|
resetNewCredForm();
|
|
showNewCred.value = false;
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
newCredError.value = msg || "Failed to save credential.";
|
|
} finally {
|
|
savingCred.value = false;
|
|
}
|
|
}
|
|
|
|
function openNew(groupId?: number): void {
|
|
isEditing.value = false;
|
|
editingId.value = null;
|
|
form.value = {
|
|
name: "",
|
|
hostname: "",
|
|
port: 22,
|
|
protocol: "ssh",
|
|
groupId: groupId ?? null,
|
|
credentialId: null,
|
|
color: "",
|
|
notes: "",
|
|
};
|
|
tagsInput.value = "";
|
|
resetNewCredForm();
|
|
showNewCred.value = false;
|
|
void loadCredentials();
|
|
visible.value = true;
|
|
}
|
|
|
|
function openEdit(conn: Connection): void {
|
|
isEditing.value = true;
|
|
editingId.value = conn.id;
|
|
form.value = {
|
|
name: conn.name,
|
|
hostname: conn.hostname,
|
|
port: conn.port,
|
|
protocol: conn.protocol,
|
|
groupId: conn.groupId,
|
|
credentialId: conn.credentialId ?? null,
|
|
color: conn.color ?? "",
|
|
notes: conn.notes ?? "",
|
|
};
|
|
tagsInput.value = conn.tags?.join(", ") ?? "";
|
|
resetNewCredForm();
|
|
showNewCred.value = false;
|
|
void loadCredentials();
|
|
visible.value = true;
|
|
}
|
|
|
|
function close(): void {
|
|
visible.value = false;
|
|
}
|
|
|
|
async function save(): Promise<void> {
|
|
if (!isValid.value) return;
|
|
|
|
const tags = tagsInput.value
|
|
.split(",")
|
|
.map((t) => t.trim())
|
|
.filter((t) => t.length > 0);
|
|
|
|
try {
|
|
if (isEditing.value && editingId.value !== null) {
|
|
await Call.ByName(`${CONN}.UpdateConnection`, editingId.value, {
|
|
name: form.value.name,
|
|
hostname: form.value.hostname,
|
|
port: form.value.port,
|
|
groupId: form.value.groupId,
|
|
credentialId: form.value.credentialId,
|
|
color: form.value.color,
|
|
tags,
|
|
notes: form.value.notes,
|
|
});
|
|
} else {
|
|
await Call.ByName(`${CONN}.CreateConnection`, {
|
|
name: form.value.name,
|
|
hostname: form.value.hostname,
|
|
port: form.value.port,
|
|
protocol: form.value.protocol,
|
|
groupId: form.value.groupId,
|
|
credentialId: form.value.credentialId,
|
|
color: form.value.color,
|
|
tags,
|
|
notes: form.value.notes,
|
|
});
|
|
}
|
|
// Refresh connections from backend
|
|
await connectionStore.loadAll();
|
|
close();
|
|
} catch (err) {
|
|
console.error("Failed to save connection:", err);
|
|
}
|
|
}
|
|
|
|
defineExpose({ openNew, openEdit, close, visible });
|
|
</script>
|