feat: SFTP sidebar — file tree with mock data and transfer progress
Add useSftp composable with mock directory listings, path navigation, and refresh. FileTree component shows path bar, toolbar (upload, download, new folder, refresh, delete icons), file entries with icons, humanized sizes, and dates. TransferProgress component shows expandable transfer list with progress bars. SidebarToggle now uses v-model to emit tab changes. MainLayout switches between ConnectionTree and FileTree based on sidebar tab, and includes TransferProgress panel. File double-click emits openFile event for the editor integration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d66849035
commit
8415c98970
165
frontend/src/components/sftp/FileTree.vue
Normal file
165
frontend/src/components/sftp/FileTree.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full text-xs">
|
||||||
|
<!-- Path bar -->
|
||||||
|
<div class="flex items-center gap-1 px-3 py-1.5 border-b border-[var(--wraith-border)] bg-[var(--wraith-bg-tertiary)]">
|
||||||
|
<button
|
||||||
|
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer shrink-0"
|
||||||
|
title="Go up"
|
||||||
|
@click="goUp"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M3.22 9.78a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1-1.06 1.06L8 6.06 4.28 9.78a.75.75 0 0 1-1.06 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-[var(--wraith-text-secondary)] truncate font-mono text-[10px]">
|
||||||
|
{{ currentPath }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="flex items-center gap-1 px-2 py-1 border-b border-[var(--wraith-border)]">
|
||||||
|
<button
|
||||||
|
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-blue)] transition-colors cursor-pointer rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
||||||
|
title="Upload file"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" 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 14H2.75z" />
|
||||||
|
<path d="M11.78 4.72a.75.75 0 0 1-1.06 1.06L8.75 3.81V9.5a.75.75 0 0 1-1.5 0V3.81L5.28 5.78a.75.75 0 0 1-1.06-1.06l3.25-3.25a.75.75 0 0 1 1.06 0l3.25 3.25z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-blue)] transition-colors cursor-pointer rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
||||||
|
title="Download file"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" 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 14H2.75z" />
|
||||||
|
<path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-yellow)] transition-colors cursor-pointer rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
||||||
|
title="New folder"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75zM8.75 8v1.75a.75.75 0 0 1-1.5 0V8H5.5a.75.75 0 0 1 0-1.5h1.75V4.75a.75.75 0 0 1 1.5 0V6.5h1.75a.75.75 0 0 1 0 1.5H8.75z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
||||||
|
title="Refresh"
|
||||||
|
@click="refresh"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.001 7.001 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.501 5.501 0 0 0 8 2.5zM1.705 8.005a.75.75 0 0 1 .834.656 5.501 5.501 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.001 7.001 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-red)] transition-colors cursor-pointer rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M11 1.75V3h2.25a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM6.5 1.75v1.25h3V1.75a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25zM4.997 6.178a.75.75 0 1 0-1.493.144l.685 7.107A2.25 2.25 0 0 0 6.427 15.5h3.146a2.25 2.25 0 0 0 2.238-2.071l.685-7.107a.75.75 0 1 0-1.493-.144l-.685 7.107a.75.75 0 0 1-.746.715H6.427a.75.75 0 0 1-.746-.715l-.684-7.107z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File list -->
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||||
|
<span class="text-[var(--wraith-text-muted)]">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<div v-else-if="entries.length === 0" class="flex items-center justify-center py-8">
|
||||||
|
<span class="text-[var(--wraith-text-muted)]">Empty directory</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Entries -->
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
v-for="entry in entries"
|
||||||
|
:key="entry.path"
|
||||||
|
class="w-full flex items-center gap-2 px-3 py-1.5 hover:bg-[var(--wraith-bg-tertiary)] transition-colors cursor-pointer group"
|
||||||
|
@dblclick="handleEntryDblClick(entry)"
|
||||||
|
>
|
||||||
|
<!-- Icon -->
|
||||||
|
<svg
|
||||||
|
v-if="entry.isDir"
|
||||||
|
class="w-3.5 h-3.5 text-[var(--wraith-accent-yellow)] shrink-0"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75z" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="w-3.5 h-3.5 text-[var(--wraith-text-muted)] shrink-0"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M3.75 1.5a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25V6H9.75A1.75 1.75 0 0 1 8 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25V1.75z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<span class="text-[var(--wraith-text-primary)] truncate">{{ entry.name }}</span>
|
||||||
|
|
||||||
|
<!-- Size (files only) -->
|
||||||
|
<span
|
||||||
|
v-if="!entry.isDir"
|
||||||
|
class="ml-auto text-[var(--wraith-text-muted)] text-[10px] shrink-0"
|
||||||
|
>
|
||||||
|
{{ humanizeSize(entry.size) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Modified date -->
|
||||||
|
<span class="text-[var(--wraith-text-muted)] text-[10px] shrink-0 w-[68px] text-right">
|
||||||
|
{{ entry.modTime }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Follow terminal toggle -->
|
||||||
|
<div class="flex items-center gap-2 px-3 py-1.5 border-t border-[var(--wraith-border)]">
|
||||||
|
<label class="flex items-center gap-1.5 cursor-pointer text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)] transition-colors">
|
||||||
|
<input
|
||||||
|
v-model="followTerminal"
|
||||||
|
type="checkbox"
|
||||||
|
class="w-3 h-3 accent-[var(--wraith-accent-blue)] cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span>Follow terminal folder</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useSftp, type FileEntry } from "@/composables/useSftp";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
sessionId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
openFile: [entry: FileEntry];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { currentPath, entries, isLoading, followTerminal, navigateTo, goUp, refresh } = useSftp(props.sessionId);
|
||||||
|
|
||||||
|
function handleEntryDblClick(entry: FileEntry): void {
|
||||||
|
if (entry.isDir) {
|
||||||
|
navigateTo(entry.path);
|
||||||
|
} else {
|
||||||
|
emit("openFile", entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanizeSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
const size = (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0);
|
||||||
|
return `${size} ${units[i]}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
72
frontend/src/components/sftp/TransferProgress.vue
Normal file
72
frontend/src/components/sftp/TransferProgress.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="border-t border-[var(--wraith-border)]">
|
||||||
|
<!-- Header -->
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center justify-between px-3 py-1.5 text-xs text-[var(--wraith-text-muted)] hover:bg-[var(--wraith-bg-tertiary)] transition-colors cursor-pointer"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
>
|
||||||
|
<span>Transfers ({{ transfers.length }})</span>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 transition-transform"
|
||||||
|
:class="{ 'rotate-180': expanded }"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M12.78 5.22a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.22 6.28a.75.75 0 0 1 1.06-1.06L8 8.94l3.72-3.72a.75.75 0 0 1 1.06 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Transfer list -->
|
||||||
|
<div v-if="expanded && transfers.length > 0" class="px-3 pb-2">
|
||||||
|
<div
|
||||||
|
v-for="transfer in transfers"
|
||||||
|
:key="transfer.id"
|
||||||
|
class="flex flex-col gap-1 py-1.5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-[var(--wraith-text-secondary)] truncate">
|
||||||
|
{{ transfer.fileName }}
|
||||||
|
</span>
|
||||||
|
<span class="text-[var(--wraith-text-muted)] shrink-0 ml-2">
|
||||||
|
{{ transfer.speed }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-1.5 bg-[var(--wraith-bg-tertiary)] rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full transition-all duration-300"
|
||||||
|
:class="transfer.direction === 'upload' ? 'bg-[var(--wraith-accent-blue)]' : 'bg-[var(--wraith-accent-green)]'"
|
||||||
|
:style="{ width: transfer.percentage + '%' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-[var(--wraith-text-muted)]">
|
||||||
|
{{ transfer.percentage }}% - {{ transfer.direction === 'upload' ? 'Uploading' : 'Downloading' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div v-if="expanded && transfers.length === 0" class="px-3 py-2 text-xs text-[var(--wraith-text-muted)]">
|
||||||
|
No active transfers
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
interface Transfer {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
percentage: number;
|
||||||
|
speed: string;
|
||||||
|
direction: "upload" | "download";
|
||||||
|
}
|
||||||
|
|
||||||
|
const expanded = ref(false);
|
||||||
|
|
||||||
|
// Placeholder mock transfers — empty by default
|
||||||
|
const transfers = ref<Transfer[]>([]);
|
||||||
|
|
||||||
|
// Exports so parent can push mock transfers for testing if needed
|
||||||
|
defineExpose({ transfers });
|
||||||
|
</script>
|
||||||
@ -5,11 +5,11 @@
|
|||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
class="flex-1 py-2 text-xs font-medium text-center transition-colors cursor-pointer"
|
class="flex-1 py-2 text-xs font-medium text-center transition-colors cursor-pointer"
|
||||||
:class="
|
:class="
|
||||||
activeTab === tab.id
|
modelValue === tab.id
|
||||||
? 'text-[var(--wraith-accent-blue)] border-b-2 border-[var(--wraith-accent-blue)]'
|
? 'text-[var(--wraith-accent-blue)] border-b-2 border-[var(--wraith-accent-blue)]'
|
||||||
: 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)]'
|
: 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)]'
|
||||||
"
|
"
|
||||||
@click="activeTab = tab.id"
|
@click="emit('update:modelValue', tab.id)"
|
||||||
>
|
>
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</button>
|
</button>
|
||||||
@ -17,12 +17,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
export type SidebarTab = "connections" | "sftp";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "connections", label: "Connections" },
|
{ id: "connections" as const, label: "Connections" },
|
||||||
{ id: "sftp", label: "SFTP" },
|
{ id: "sftp" as const, label: "SFTP" },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
const activeTab = ref<"connections" | "sftp">("connections");
|
defineProps<{
|
||||||
|
modelValue: SidebarTab;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [tab: SidebarTab];
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
100
frontend/src/composables/useSftp.ts
Normal file
100
frontend/src/composables/useSftp.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { ref, type Ref } from "vue";
|
||||||
|
|
||||||
|
export interface FileEntry {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
isDir: boolean;
|
||||||
|
permissions: string;
|
||||||
|
modTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseSftpReturn {
|
||||||
|
currentPath: Ref<string>;
|
||||||
|
entries: Ref<FileEntry[]>;
|
||||||
|
isLoading: Ref<boolean>;
|
||||||
|
followTerminal: Ref<boolean>;
|
||||||
|
navigateTo: (path: string) => Promise<void>;
|
||||||
|
goUp: () => Promise<void>;
|
||||||
|
refresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mock directory listings used until Wails SFTP bindings are connected. */
|
||||||
|
const mockDirectories: Record<string, FileEntry[]> = {
|
||||||
|
"/home/user": [
|
||||||
|
{ name: "docs", path: "/home/user/docs", size: 0, isDir: true, permissions: "drwxr-xr-x", modTime: "2026-03-17" },
|
||||||
|
{ name: "projects", path: "/home/user/projects", size: 0, isDir: true, permissions: "drwxr-xr-x", modTime: "2026-03-16" },
|
||||||
|
{ name: ".ssh", path: "/home/user/.ssh", size: 0, isDir: true, permissions: "drwx------", modTime: "2026-03-10" },
|
||||||
|
{ name: ".bashrc", path: "/home/user/.bashrc", size: 3771, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-15" },
|
||||||
|
{ name: "deploy.sh", path: "/home/user/deploy.sh", size: 1024, isDir: false, permissions: "-rwxr-xr-x", modTime: "2026-03-16" },
|
||||||
|
{ name: ".profile", path: "/home/user/.profile", size: 807, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-10" },
|
||||||
|
],
|
||||||
|
"/home/user/docs": [
|
||||||
|
{ name: "readme.md", path: "/home/user/docs/readme.md", size: 2048, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-17" },
|
||||||
|
{ name: "notes.txt", path: "/home/user/docs/notes.txt", size: 512, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-14" },
|
||||||
|
],
|
||||||
|
"/home/user/projects": [
|
||||||
|
{ name: "app", path: "/home/user/projects/app", size: 0, isDir: true, permissions: "drwxr-xr-x", modTime: "2026-03-16" },
|
||||||
|
{ name: "Makefile", path: "/home/user/projects/Makefile", size: 256, isDir: false, permissions: "-rw-r--r--", modTime: "2026-03-16" },
|
||||||
|
],
|
||||||
|
"/home/user/.ssh": [
|
||||||
|
{ name: "authorized_keys", path: "/home/user/.ssh/authorized_keys", size: 743, isDir: false, permissions: "-rw-------", modTime: "2026-03-10" },
|
||||||
|
{ name: "config", path: "/home/user/.ssh/config", size: 128, isDir: false, permissions: "-rw-------", modTime: "2026-03-10" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable that manages SFTP file browsing state.
|
||||||
|
*
|
||||||
|
* Uses mock data until Wails SFTPService bindings are connected.
|
||||||
|
*/
|
||||||
|
export function useSftp(_sessionId: string): UseSftpReturn {
|
||||||
|
const currentPath = ref("/home/user");
|
||||||
|
const entries = ref<FileEntry[]>([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const followTerminal = ref(false);
|
||||||
|
|
||||||
|
async function listDirectory(path: string): Promise<FileEntry[]> {
|
||||||
|
// TODO: Replace with Wails binding call — SFTPService.List(sessionId, path)
|
||||||
|
// Simulate network delay
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
return mockDirectories[path] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateTo(path: string): Promise<void> {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
currentPath.value = path;
|
||||||
|
entries.value = await listDirectory(path);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goUp(): Promise<void> {
|
||||||
|
const parts = currentPath.value.split("/").filter(Boolean);
|
||||||
|
if (parts.length <= 1) {
|
||||||
|
await navigateTo("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parts.pop();
|
||||||
|
await navigateTo("/" + parts.join("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(): Promise<void> {
|
||||||
|
await navigateTo(currentPath.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial directory
|
||||||
|
navigateTo(currentPath.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPath,
|
||||||
|
entries,
|
||||||
|
isLoading,
|
||||||
|
followTerminal,
|
||||||
|
navigateTo,
|
||||||
|
goUp,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -33,10 +33,10 @@
|
|||||||
class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0"
|
class="flex flex-col bg-[var(--wraith-bg-secondary)] border-r border-[var(--wraith-border)] shrink-0"
|
||||||
:style="{ width: sidebarWidth + 'px' }"
|
:style="{ width: sidebarWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<SidebarToggle />
|
<SidebarToggle v-model="sidebarTab" />
|
||||||
|
|
||||||
<!-- Search -->
|
<!-- Search (connections mode only) -->
|
||||||
<div class="px-3 py-2">
|
<div v-if="sidebarTab === 'connections'" class="px-3 py-2">
|
||||||
<input
|
<input
|
||||||
v-model="connectionStore.searchQuery"
|
v-model="connectionStore.searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
@ -45,10 +45,28 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection tree -->
|
<!-- Sidebar content -->
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<ConnectionTree />
|
<!-- Connection tree -->
|
||||||
|
<ConnectionTree v-if="sidebarTab === 'connections'" />
|
||||||
|
|
||||||
|
<!-- SFTP file tree -->
|
||||||
|
<template v-else-if="sidebarTab === 'sftp'">
|
||||||
|
<FileTree
|
||||||
|
v-if="sessionStore.activeSession && sessionStore.activeSession.protocol === 'ssh'"
|
||||||
|
:session-id="sessionStore.activeSession.id"
|
||||||
|
@open-file="handleOpenFile"
|
||||||
|
/>
|
||||||
|
<div v-else class="flex items-center justify-center py-8 px-3">
|
||||||
|
<p class="text-[var(--wraith-text-muted)] text-xs text-center">
|
||||||
|
Connect to an SSH session to browse files
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transfer progress (SFTP mode only) -->
|
||||||
|
<TransferProgress v-if="sidebarTab === 'sftp'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content area -->
|
<!-- Content area -->
|
||||||
@ -56,6 +74,15 @@
|
|||||||
<!-- Tab bar -->
|
<!-- Tab bar -->
|
||||||
<TabBar />
|
<TabBar />
|
||||||
|
|
||||||
|
<!-- Editor panel (if a file is open) -->
|
||||||
|
<EditorWindow
|
||||||
|
v-if="editorFile"
|
||||||
|
:content="editorFile.content"
|
||||||
|
:file-path="editorFile.path"
|
||||||
|
:session-id="editorFile.sessionId"
|
||||||
|
@close="editorFile = null"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Session area -->
|
<!-- Session area -->
|
||||||
<SessionContainer />
|
<SessionContainer />
|
||||||
</div>
|
</div>
|
||||||
@ -73,13 +100,42 @@ import { useConnectionStore } from "@/stores/connection.store";
|
|||||||
import { useSessionStore } from "@/stores/session.store";
|
import { useSessionStore } from "@/stores/session.store";
|
||||||
import SidebarToggle from "@/components/sidebar/SidebarToggle.vue";
|
import SidebarToggle from "@/components/sidebar/SidebarToggle.vue";
|
||||||
import ConnectionTree from "@/components/sidebar/ConnectionTree.vue";
|
import ConnectionTree from "@/components/sidebar/ConnectionTree.vue";
|
||||||
|
import FileTree from "@/components/sftp/FileTree.vue";
|
||||||
|
import TransferProgress from "@/components/sftp/TransferProgress.vue";
|
||||||
import TabBar from "@/components/session/TabBar.vue";
|
import TabBar from "@/components/session/TabBar.vue";
|
||||||
import SessionContainer from "@/components/session/SessionContainer.vue";
|
import SessionContainer from "@/components/session/SessionContainer.vue";
|
||||||
import StatusBar from "@/components/common/StatusBar.vue";
|
import StatusBar from "@/components/common/StatusBar.vue";
|
||||||
|
import EditorWindow from "@/components/editor/EditorWindow.vue";
|
||||||
|
import type { SidebarTab } from "@/components/sidebar/SidebarToggle.vue";
|
||||||
|
import type { FileEntry } from "@/composables/useSftp";
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const connectionStore = useConnectionStore();
|
const connectionStore = useConnectionStore();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
const sidebarWidth = ref(240);
|
const sidebarWidth = ref(240);
|
||||||
|
const sidebarTab = ref<SidebarTab>("connections");
|
||||||
|
|
||||||
|
/** Currently open file in the editor panel (null = no file open). */
|
||||||
|
const editorFile = ref<{ content: string; path: string; sessionId: string } | null>(null);
|
||||||
|
|
||||||
|
/** Handle file open from SFTP sidebar — loads mock content for now. */
|
||||||
|
function handleOpenFile(entry: FileEntry): void {
|
||||||
|
if (!sessionStore.activeSession) return;
|
||||||
|
|
||||||
|
// TODO: Replace with Wails binding call — SFTPService.ReadFile(sessionId, entry.path)
|
||||||
|
// Mock file content for development
|
||||||
|
const mockContent = `# ${entry.name}\n\n` +
|
||||||
|
`# File: ${entry.path}\n` +
|
||||||
|
`# Size: ${entry.size} bytes\n` +
|
||||||
|
`# Permissions: ${entry.permissions}\n` +
|
||||||
|
`# Modified: ${entry.modTime}\n\n` +
|
||||||
|
`# TODO: Content will be loaded from SFTPService.ReadFile()\n`;
|
||||||
|
|
||||||
|
editorFile.value = {
|
||||||
|
content: mockContent,
|
||||||
|
path: entry.path,
|
||||||
|
sessionId: sessionStore.activeSession.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user