Rust SSH service: russh async client, DashMap session registry, TOFU host key verification, CWD tracking via separate exec channel (never touches terminal stream), base64 event emission for terminal I/O. 52/52 tests passing. Vue 3 frontend: ported from Wails v3 to Tauri v2 — useTerminal composable with streaming TextDecoder + rAF batching, session store with multi-connection support, connection store/tree, sidebar, tab bar, status bar, keyboard shortcuts. All Wails imports replaced with Tauri API equivalents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
111 lines
2.9 KiB
TypeScript
111 lines
2.9 KiB
TypeScript
import { defineStore } from "pinia";
|
|
import { ref, computed } from "vue";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
export interface Connection {
|
|
id: number;
|
|
name: string;
|
|
hostname: string;
|
|
port: number;
|
|
protocol: "ssh" | "rdp";
|
|
groupId: number | null;
|
|
credentialId?: number | null;
|
|
color?: string;
|
|
tags?: string[];
|
|
notes?: string;
|
|
options?: string;
|
|
sortOrder?: number;
|
|
lastConnected?: string | null;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
export interface Group {
|
|
id: number;
|
|
name: string;
|
|
parentId: number | null;
|
|
sortOrder?: number;
|
|
icon?: string;
|
|
children?: Group[];
|
|
}
|
|
|
|
/**
|
|
* Connection store.
|
|
* Manages connections, groups, and search state.
|
|
* Loads data from the Rust backend via Tauri invoke.
|
|
*/
|
|
export const useConnectionStore = defineStore("connection", () => {
|
|
const connections = ref<Connection[]>([]);
|
|
const groups = ref<Group[]>([]);
|
|
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 the Rust backend. */
|
|
async function loadConnections(): Promise<void> {
|
|
try {
|
|
const conns = await invoke<Connection[]>("list_connections");
|
|
connections.value = conns || [];
|
|
} catch (err) {
|
|
console.error("Failed to load connections:", err);
|
|
connections.value = [];
|
|
}
|
|
}
|
|
|
|
/** Load groups from the Rust backend. */
|
|
async function loadGroups(): Promise<void> {
|
|
try {
|
|
const grps = await invoke<Group[]>("list_groups");
|
|
groups.value = grps || [];
|
|
} catch (err) {
|
|
console.error("Failed to load groups:", err);
|
|
groups.value = [];
|
|
}
|
|
}
|
|
|
|
/** Load both connections and groups from the Rust backend. */
|
|
async function loadAll(): Promise<void> {
|
|
await Promise.all([loadConnections(), loadGroups()]);
|
|
}
|
|
|
|
return {
|
|
connections,
|
|
groups,
|
|
searchQuery,
|
|
filteredConnections,
|
|
connectionsByGroup,
|
|
groupHasResults,
|
|
loadConnections,
|
|
loadGroups,
|
|
loadAll,
|
|
};
|
|
});
|