Replace all TODO stubs in frontend stores with real Wails Call.ByName bindings. The app store now calls WraithApp.IsFirstRun/CreateVault/Unlock so vault state persists across launches. The connection store loads from ConnectionService.ListConnections/ListGroups instead of hardcoded mock data. The import dialog calls a new WraithApp.ImportMobaConf method that parses the file, creates groups and connections in SQLite, and stores host keys. ConnectionEditDialog also uses real Go CRUD calls. MainLayout loads connections on mount after vault unlock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
278 lines
10 KiB
Vue
278 lines
10 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)]">Import Configuration</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">
|
|
<!-- Step 1: File selection -->
|
|
<template v-if="step === 'select'">
|
|
<p class="text-sm text-[var(--wraith-text-secondary)] mb-4">
|
|
Select a MobaXTerm <code class="text-[var(--wraith-accent-blue)]">.mobaconf</code> file to import connections and settings.
|
|
</p>
|
|
<div
|
|
class="border-2 border-dashed border-[#30363d] rounded-lg p-8 text-center hover:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
|
|
@click="selectFile"
|
|
@dragover.prevent
|
|
@drop.prevent="handleDrop"
|
|
>
|
|
<svg class="w-8 h-8 mx-auto text-[var(--wraith-text-muted)] mb-3" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14ZM11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.81V9.5a.75.75 0 0 1-1.5 0V3.81L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z" />
|
|
</svg>
|
|
<p class="text-sm text-[var(--wraith-text-secondary)]">
|
|
Click to select or drag and drop
|
|
</p>
|
|
<p class="text-xs text-[var(--wraith-text-muted)] mt-1">
|
|
Supports .mobaconf files
|
|
</p>
|
|
</div>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept=".mobaconf"
|
|
class="hidden"
|
|
@change="handleFileSelect"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Step 2: Preview -->
|
|
<template v-else-if="step === 'preview'">
|
|
<div class="space-y-3">
|
|
<p class="text-sm text-[var(--wraith-text-primary)] font-medium">
|
|
{{ fileName }}
|
|
</p>
|
|
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<div class="bg-[#0d1117] rounded-lg p-3 text-center">
|
|
<div class="text-lg font-bold text-[var(--wraith-accent-blue)]">{{ preview.connections }}</div>
|
|
<div class="text-[10px] text-[var(--wraith-text-muted)] mt-0.5">Connections</div>
|
|
</div>
|
|
<div class="bg-[#0d1117] rounded-lg p-3 text-center">
|
|
<div class="text-lg font-bold text-[var(--wraith-accent-yellow)]">{{ preview.groups }}</div>
|
|
<div class="text-[10px] text-[var(--wraith-text-muted)] mt-0.5">Groups</div>
|
|
</div>
|
|
<div class="bg-[#0d1117] rounded-lg p-3 text-center">
|
|
<div class="text-lg font-bold text-[var(--wraith-accent-green)]">{{ preview.hostKeys }}</div>
|
|
<div class="text-[10px] text-[var(--wraith-text-muted)] mt-0.5">Host Keys</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="preview.hasTheme" class="flex items-center gap-2 text-xs text-[var(--wraith-text-secondary)] bg-[#0d1117] rounded-lg px-3 py-2">
|
|
<svg class="w-3.5 h-3.5 text-[var(--wraith-accent-green)]" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
|
</svg>
|
|
Terminal theme included
|
|
</div>
|
|
|
|
<p class="text-xs text-[var(--wraith-text-muted)]">
|
|
Passwords are not imported (MobaXTerm uses proprietary encryption).
|
|
</p>
|
|
|
|
<div
|
|
v-if="importError"
|
|
class="text-sm text-[var(--wraith-accent-red)] bg-[var(--wraith-accent-red)]/10 border border-[var(--wraith-accent-red)]/20 rounded px-3 py-2"
|
|
>
|
|
{{ importError }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Step 3: Complete -->
|
|
<template v-else-if="step === 'complete'">
|
|
<div class="text-center py-4">
|
|
<svg class="w-12 h-12 mx-auto text-[var(--wraith-accent-green)] mb-3" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm3.78-9.72a.751.751 0 0 0-.018-1.042.751.751 0 0 0-1.042-.018L6.75 9.19 5.28 7.72a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042l2 2a.75.75 0 0 0 1.06 0l4.5-4.5Z" />
|
|
</svg>
|
|
<h4 class="text-sm font-semibold text-[var(--wraith-text-primary)] mb-1">Import Complete</h4>
|
|
<p class="text-xs text-[var(--wraith-text-secondary)]">
|
|
Successfully imported {{ preview.connections }} connections and {{ preview.groups }} groups.
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-end gap-2 px-4 py-3 border-t border-[#30363d]">
|
|
<button
|
|
v-if="step !== 'complete'"
|
|
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
|
|
v-if="step === 'preview'"
|
|
:disabled="importing"
|
|
class="px-3 py-1.5 text-xs text-white bg-[#238636] hover:bg-[#2ea043] rounded transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="doImport"
|
|
>
|
|
{{ importing ? "Importing..." : "Import" }}
|
|
</button>
|
|
<button
|
|
v-if="step === 'complete'"
|
|
class="px-3 py-1.5 text-xs text-white bg-[#1f6feb] hover:bg-[#388bfd] rounded transition-colors cursor-pointer"
|
|
@click="close"
|
|
>
|
|
Done
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from "vue";
|
|
import { Call } from "@wailsio/runtime";
|
|
import { useConnectionStore } from "@/stores/connection.store";
|
|
|
|
/** Fully qualified Go method name prefix for WraithApp bindings. */
|
|
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
|
|
|
|
type Step = "select" | "preview" | "complete";
|
|
|
|
const connectionStore = useConnectionStore();
|
|
|
|
const visible = ref(false);
|
|
const step = ref<Step>("select");
|
|
const fileName = ref("");
|
|
const fileInput = ref<HTMLInputElement | null>(null);
|
|
const fileContent = ref("");
|
|
const importError = ref<string | null>(null);
|
|
const importing = ref(false);
|
|
|
|
const preview = ref({
|
|
connections: 0,
|
|
groups: 0,
|
|
hostKeys: 0,
|
|
hasTheme: false,
|
|
});
|
|
|
|
function open(): void {
|
|
visible.value = true;
|
|
step.value = "select";
|
|
fileName.value = "";
|
|
fileContent.value = "";
|
|
importError.value = null;
|
|
}
|
|
|
|
function close(): void {
|
|
visible.value = false;
|
|
}
|
|
|
|
function selectFile(): void {
|
|
fileInput.value?.click();
|
|
}
|
|
|
|
function handleFileSelect(event: Event): void {
|
|
const target = event.target as HTMLInputElement;
|
|
const file = target.files?.[0];
|
|
if (file) {
|
|
processFile(file);
|
|
}
|
|
}
|
|
|
|
function handleDrop(event: DragEvent): void {
|
|
const file = event.dataTransfer?.files?.[0];
|
|
if (file) {
|
|
processFile(file);
|
|
}
|
|
}
|
|
|
|
async function processFile(file: File): Promise<void> {
|
|
fileName.value = file.name;
|
|
|
|
const text = await file.text();
|
|
fileContent.value = text;
|
|
|
|
// Client-side preview parse to count items for display
|
|
const lines = text.split("\n");
|
|
let groups = 0;
|
|
let connections = 0;
|
|
let hostKeys = 0;
|
|
let hasTheme = false;
|
|
let inBookmarks = false;
|
|
let inHostKeys = false;
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (trimmed.startsWith("[Bookmarks")) {
|
|
inBookmarks = true;
|
|
inHostKeys = false;
|
|
continue;
|
|
}
|
|
if (trimmed === "[SSH_Hostkeys]") {
|
|
inBookmarks = false;
|
|
inHostKeys = true;
|
|
continue;
|
|
}
|
|
if (trimmed === "[Colors]") {
|
|
hasTheme = true;
|
|
inBookmarks = false;
|
|
inHostKeys = false;
|
|
continue;
|
|
}
|
|
if (trimmed.startsWith("[")) {
|
|
inBookmarks = false;
|
|
inHostKeys = false;
|
|
continue;
|
|
}
|
|
|
|
if (inBookmarks) {
|
|
if (trimmed.startsWith("SubRep=") && trimmed !== "SubRep=") {
|
|
groups++;
|
|
} else if (trimmed.includes("#109#") || trimmed.includes("#91#")) {
|
|
connections++;
|
|
}
|
|
}
|
|
|
|
if (inHostKeys && trimmed.includes("@") && trimmed.includes("=")) {
|
|
hostKeys++;
|
|
}
|
|
}
|
|
|
|
preview.value = { connections, groups, hostKeys, hasTheme };
|
|
step.value = "preview";
|
|
}
|
|
|
|
async function doImport(): Promise<void> {
|
|
if (!fileContent.value) return;
|
|
importing.value = true;
|
|
importError.value = null;
|
|
|
|
try {
|
|
await Call.ByName(`${APP}.ImportMobaConf`, fileContent.value);
|
|
// Refresh connection store so sidebar shows imported connections
|
|
await connectionStore.loadAll();
|
|
step.value = "complete";
|
|
} catch (e) {
|
|
importError.value = e instanceof Error ? e.message : String(e) || "Import failed";
|
|
} finally {
|
|
importing.value = false;
|
|
}
|
|
}
|
|
|
|
defineExpose({ open, close, visible });
|
|
</script>
|