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 @@ + + + + + + + + {{ sessionStore.activeSession.protocol.toUpperCase() }} + + · + {{ connectionInfo }} + + + Ready + + + + + + Theme: Dracula + UTF-8 + 120×40 + + + + + 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 @@ + + + + + Session: {{ sessionStore.activeSession.name }} + + + {{ sessionStore.activeSession.protocol.toUpperCase() }} terminal will render here + + + + + No active session + + + Double-click a connection to start a session + + + + + + 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 @@ + + + + + + + + + {{ session.name }} + + + + × + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + {{ group.name }} + + + + {{ connectionStore.connectionsByGroup(group.id).length }} + + + + + + + + + {{ conn.name }} + + {{ tag }} + + + + + + + + + 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 @@ + + + + {{ tab.label }} + + + + + 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 @@ + + + + + + WRAITH + + + {{ sessionStore.sessionCount }} session{{ sessionStore.sessionCount !== 1 ? "s" : "" }} + + 🔒 + + + ⚙ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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, + }; +});
+ Session: {{ sessionStore.activeSession.name }} +
+ {{ sessionStore.activeSession.protocol.toUpperCase() }} terminal will render here +
+ No active session +
+ Double-click a connection to start a session +