feat(ui): add MobaXTerm import dialog with file picker and preview
Three-step import flow: file selection (click or drag-and-drop), preview showing connection/group/host-key counts, and success confirmation. Parses .mobaconf files client-side for preview; actual import will use Wails binding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
326fa9530f
commit
f5de568b32
247
frontend/src/components/common/ImportDialog.vue
Normal file
247
frontend/src/components/common/ImportDialog.vue
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<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>
|
||||||
|
</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'"
|
||||||
|
class="px-3 py-1.5 text-xs text-white bg-[#238636] hover:bg-[#2ea043] rounded transition-colors cursor-pointer"
|
||||||
|
@click="doImport"
|
||||||
|
>
|
||||||
|
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";
|
||||||
|
|
||||||
|
type Step = "select" | "preview" | "complete";
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const step = ref<Step>("select");
|
||||||
|
const fileName = ref("");
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const preview = ref({
|
||||||
|
connections: 0,
|
||||||
|
groups: 0,
|
||||||
|
hostKeys: 0,
|
||||||
|
hasTheme: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function open(): void {
|
||||||
|
visible.value = true;
|
||||||
|
step.value = "select";
|
||||||
|
fileName.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// TODO: Replace with Wails binding — ImporterService.Preview(fileData)
|
||||||
|
// For now, read and mock-parse the file to show a preview
|
||||||
|
const text = await file.text();
|
||||||
|
|
||||||
|
// Simple mock parse to count items
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
function doImport(): void {
|
||||||
|
// TODO: Replace with Wails binding — ImporterService.Import(fileData)
|
||||||
|
// For now, just show success
|
||||||
|
step.value = "complete";
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open, close, visible });
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue
Block a user