From d57cd6cfbb2ee9ea5b3ae90bcb3ef9d9d095b59d Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Tue, 17 Mar 2026 06:32:17 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20main=20layout=20=E2=80=94=20sidebar=20c?= =?UTF-8?q?onnection=20tree,=20tab=20bar,=20status=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-panel layout with 240px sidebar, tabbed session area, and status bar. Sidebar has Connections/SFTP toggle, search input, and collapsible group tree with protocol-colored dots. Tab bar shows active sessions with color-coded indicators and close buttons. Status bar displays connection info, theme, encoding, and terminal size. All backed by connection and session Pinia stores with mock data until Wails bindings are connected. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/common/StatusBar.vue | 47 +++++++++ .../components/session/SessionContainer.vue | 26 +++++ frontend/src/components/session/TabBar.vue | 48 ++++++++++ .../src/components/sidebar/ConnectionTree.vue | 83 ++++++++++++++++ .../src/components/sidebar/SidebarToggle.vue | 28 ++++++ frontend/src/layouts/MainLayout.vue | 85 +++++++++++++++++ frontend/src/stores/connection.store.ts | 95 +++++++++++++++++++ frontend/src/stores/session.store.ts | 73 ++++++++++++++ 8 files changed, 485 insertions(+) create mode 100644 frontend/src/components/common/StatusBar.vue create mode 100644 frontend/src/components/session/SessionContainer.vue create mode 100644 frontend/src/components/session/TabBar.vue create mode 100644 frontend/src/components/sidebar/ConnectionTree.vue create mode 100644 frontend/src/components/sidebar/SidebarToggle.vue create mode 100644 frontend/src/layouts/MainLayout.vue create mode 100644 frontend/src/stores/connection.store.ts create mode 100644 frontend/src/stores/session.store.ts diff --git a/frontend/src/components/common/StatusBar.vue b/frontend/src/components/common/StatusBar.vue new file mode 100644 index 0000000..b26b5fd --- /dev/null +++ b/frontend/src/components/common/StatusBar.vue @@ -0,0 +1,47 @@ + + + diff --git a/frontend/src/components/session/SessionContainer.vue b/frontend/src/components/session/SessionContainer.vue new file mode 100644 index 0000000..f0dd889 --- /dev/null +++ b/frontend/src/components/session/SessionContainer.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/session/TabBar.vue b/frontend/src/components/session/TabBar.vue new file mode 100644 index 0000000..0c74c3a --- /dev/null +++ b/frontend/src/components/session/TabBar.vue @@ -0,0 +1,48 @@ + + + diff --git a/frontend/src/components/sidebar/ConnectionTree.vue b/frontend/src/components/sidebar/ConnectionTree.vue new file mode 100644 index 0000000..734ca11 --- /dev/null +++ b/frontend/src/components/sidebar/ConnectionTree.vue @@ -0,0 +1,83 @@ + + + diff --git a/frontend/src/components/sidebar/SidebarToggle.vue b/frontend/src/components/sidebar/SidebarToggle.vue new file mode 100644 index 0000000..fc07ab9 --- /dev/null +++ b/frontend/src/components/sidebar/SidebarToggle.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue new file mode 100644 index 0000000..237db2f --- /dev/null +++ b/frontend/src/layouts/MainLayout.vue @@ -0,0 +1,85 @@ + + + diff --git a/frontend/src/stores/connection.store.ts b/frontend/src/stores/connection.store.ts new file mode 100644 index 0000000..f6193c0 --- /dev/null +++ b/frontend/src/stores/connection.store.ts @@ -0,0 +1,95 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; + +export interface Connection { + id: number; + name: string; + hostname: string; + port: number; + protocol: "ssh" | "rdp"; + groupId: number; + tags?: string[]; +} + +export interface Group { + id: number; + name: string; + parentId: number | null; +} + +/** + * Connection store. + * Manages connections, groups, and search state. + * + * Uses mock data until Wails bindings are connected. + */ +export const useConnectionStore = defineStore("connection", () => { + const connections = ref([ + { id: 1, name: "Asgard", hostname: "192.168.1.4", port: 22, protocol: "ssh", groupId: 1, tags: ["Prod"] }, + { id: 2, name: "Docker", hostname: "155.254.29.221", port: 22, protocol: "ssh", groupId: 1, tags: ["Prod"] }, + { id: 3, name: "Predator Mac", hostname: "192.168.1.214", port: 22, protocol: "ssh", groupId: 1 }, + { id: 4, name: "CLT-VMHOST01", hostname: "100.64.1.204", port: 3389, protocol: "rdp", groupId: 1 }, + { id: 5, name: "ITFlow", hostname: "192.154.253.106", port: 22, protocol: "ssh", groupId: 2 }, + { id: 6, name: "Mautic", hostname: "192.154.253.112", port: 22, protocol: "ssh", groupId: 2 }, + ]); + + const groups = ref([ + { id: 1, name: "Vantz's Stuff", parentId: null }, + { id: 2, name: "MSPNerd", parentId: null }, + ]); + + const searchQuery = ref(""); + + /** Filter connections by search query. */ + const filteredConnections = computed(() => { + const q = searchQuery.value.toLowerCase().trim(); + if (!q) return connections.value; + return connections.value.filter( + (c) => + c.name.toLowerCase().includes(q) || + c.hostname.toLowerCase().includes(q) || + c.tags?.some((t) => t.toLowerCase().includes(q)), + ); + }); + + /** Get connections belonging to a specific group. */ + function connectionsByGroup(groupId: number): Connection[] { + const q = searchQuery.value.toLowerCase().trim(); + const groupConns = connections.value.filter((c) => c.groupId === groupId); + if (!q) return groupConns; + return groupConns.filter( + (c) => + c.name.toLowerCase().includes(q) || + c.hostname.toLowerCase().includes(q) || + c.tags?.some((t) => t.toLowerCase().includes(q)), + ); + } + + /** Check if a group has any matching connections (for search filtering). */ + function groupHasResults(groupId: number): boolean { + return connectionsByGroup(groupId).length > 0; + } + + /** Load connections from backend (mock for now). */ + async function loadConnections(): Promise { + // TODO: replace with Wails binding — ConnectionService.ListConnections() + // connections.value = await ConnectionService.ListConnections(); + } + + /** Load groups from backend (mock for now). */ + async function loadGroups(): Promise { + // TODO: replace with Wails binding — ConnectionService.ListGroups() + // groups.value = await ConnectionService.ListGroups(); + } + + return { + connections, + groups, + searchQuery, + filteredConnections, + connectionsByGroup, + groupHasResults, + loadConnections, + loadGroups, + }; +}); diff --git a/frontend/src/stores/session.store.ts b/frontend/src/stores/session.store.ts new file mode 100644 index 0000000..2390e18 --- /dev/null +++ b/frontend/src/stores/session.store.ts @@ -0,0 +1,73 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; + +export interface Session { + id: string; + connectionId: number; + name: string; + protocol: "ssh" | "rdp"; + active: boolean; +} + +/** + * Session store. + * Manages active sessions and tab order. + * + * Sessions are populated by the Go SessionManager once plugins are wired up. + * For now, mock sessions are used to render the tab bar. + */ +export const useSessionStore = defineStore("session", () => { + const sessions = ref([ + { id: "s1", connectionId: 1, name: "Asgard", protocol: "ssh", active: true }, + { id: "s2", connectionId: 2, name: "Docker", protocol: "ssh", active: false }, + { id: "s3", connectionId: 4, name: "CLT-VMHOST01", protocol: "rdp", active: false }, + ]); + + const activeSessionId = ref("s1"); + + const activeSession = computed(() => + sessions.value.find((s) => s.id === activeSessionId.value) ?? null, + ); + + const sessionCount = computed(() => sessions.value.length); + + /** Switch to a session tab. */ + function activateSession(id: string): void { + activeSessionId.value = id; + } + + /** Close a session tab. */ + function closeSession(id: string): void { + const idx = sessions.value.findIndex((s) => s.id === id); + if (idx === -1) return; + + sessions.value.splice(idx, 1); + + // If we closed the active session, activate an adjacent one + if (activeSessionId.value === id) { + if (sessions.value.length === 0) { + activeSessionId.value = null; + } else { + const nextIdx = Math.min(idx, sessions.value.length - 1); + activeSessionId.value = sessions.value[nextIdx].id; + } + } + } + + /** Add a new session (placeholder — will be called from connection double-click). */ + function addSession(connectionId: number, name: string, protocol: "ssh" | "rdp"): void { + const id = `s${Date.now()}`; + sessions.value.push({ id, connectionId, name, protocol, active: false }); + activeSessionId.value = id; + } + + return { + sessions, + activeSessionId, + activeSession, + sessionCount, + activateSession, + closeSession, + addSession, + }; +});