feat: connection status indicators + draggable tab reordering
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 2m52s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 2m52s
Status indicators: - Session.status field tracks connected/disconnected state - Listens for ssh:close and ssh:exit backend events - Tab dot turns red when connection drops (green=SSH, blue=RDP, red=dead) Draggable tabs: - HTML5 drag-and-drop on tab buttons - Blue left-border indicator shows drop target - moveSession() in store reorders the sessions array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
087b00c886
commit
02b3ee053d
@ -36,12 +36,15 @@ const props = defineProps<{
|
|||||||
username?: string;
|
username?: string;
|
||||||
/** Raw tags from the connection record. */
|
/** Raw tags from the connection record. */
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
/** Connection status — drives the dot colour. */
|
||||||
|
status?: "connected" | "disconnected";
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/** Green for SSH, blue for RDP. */
|
/** Green=connected SSH, blue=connected RDP, red=disconnected. */
|
||||||
const protocolDotClass = computed(() =>
|
const protocolDotClass = computed(() => {
|
||||||
props.protocol === "ssh" ? "bg-[#3fb950]" : "bg-[#1f6feb]",
|
if (props.status === "disconnected") return "bg-[#f85149]";
|
||||||
);
|
return props.protocol === "ssh" ? "bg-[#3fb950]" : "bg-[#1f6feb]";
|
||||||
|
});
|
||||||
|
|
||||||
/** True when the session is running as root or Administrator. */
|
/** True when the session is running as root or Administrator. */
|
||||||
const isRoot = computed(() => {
|
const isRoot = computed(() => {
|
||||||
|
|||||||
@ -3,22 +3,30 @@
|
|||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="flex items-center overflow-x-auto min-w-0">
|
<div class="flex items-center overflow-x-auto min-w-0">
|
||||||
<button
|
<button
|
||||||
v-for="session in sessionStore.sessions"
|
v-for="(session, index) in sessionStore.sessions"
|
||||||
:key="session.id"
|
:key="session.id"
|
||||||
|
draggable="true"
|
||||||
class="group flex items-center gap-1.5 px-3 h-9 text-xs whitespace-nowrap border-r border-[var(--wraith-border)] transition-all duration-500 cursor-pointer shrink-0"
|
class="group flex items-center gap-1.5 px-3 h-9 text-xs whitespace-nowrap border-r border-[var(--wraith-border)] transition-all duration-500 cursor-pointer shrink-0"
|
||||||
:class="[
|
:class="[
|
||||||
session.id === sessionStore.activeSessionId
|
session.id === sessionStore.activeSessionId
|
||||||
? 'bg-[var(--wraith-bg-primary)] text-[var(--wraith-text-primary)] border-b-2 border-b-[var(--wraith-accent-blue)]'
|
? 'bg-[var(--wraith-bg-primary)] text-[var(--wraith-text-primary)] border-b-2 border-b-[var(--wraith-accent-blue)]'
|
||||||
: 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)] hover:bg-[var(--wraith-bg-tertiary)]',
|
: 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)] hover:bg-[var(--wraith-bg-tertiary)]',
|
||||||
isRootUser(session) ? 'border-t-2 border-t-[#f8514966]' : '',
|
isRootUser(session) ? 'border-t-2 border-t-[#f8514966]' : '',
|
||||||
|
dragOverIndex === index ? 'border-l-2 border-l-[var(--wraith-accent-blue)]' : '',
|
||||||
]"
|
]"
|
||||||
@click="sessionStore.activateSession(session.id)"
|
@click="sessionStore.activateSession(session.id)"
|
||||||
|
@dragstart="onDragStart(index, $event)"
|
||||||
|
@dragover.prevent="onDragOver(index)"
|
||||||
|
@dragleave="dragOverIndex = -1"
|
||||||
|
@drop.prevent="onDrop(index)"
|
||||||
|
@dragend="draggedIndex = -1; dragOverIndex = -1"
|
||||||
>
|
>
|
||||||
<!-- Badge: protocol dot + root dot + env pills -->
|
<!-- Badge: protocol dot + root dot + env pills -->
|
||||||
<TabBadge
|
<TabBadge
|
||||||
:protocol="session.protocol"
|
:protocol="session.protocol"
|
||||||
:username="session.username"
|
:username="session.username"
|
||||||
:tags="getSessionTags(session)"
|
:tags="getSessionTags(session)"
|
||||||
|
:status="session.status"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span>{{ session.name }}</span>
|
<span>{{ session.name }}</span>
|
||||||
@ -44,6 +52,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
import { useSessionStore, type Session } from "@/stores/session.store";
|
import { useSessionStore, type Session } from "@/stores/session.store";
|
||||||
import { useConnectionStore } from "@/stores/connection.store";
|
import { useConnectionStore } from "@/stores/connection.store";
|
||||||
import TabBadge from "@/components/session/TabBadge.vue";
|
import TabBadge from "@/components/session/TabBadge.vue";
|
||||||
@ -51,6 +60,31 @@ import TabBadge from "@/components/session/TabBadge.vue";
|
|||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const connectionStore = useConnectionStore();
|
const connectionStore = useConnectionStore();
|
||||||
|
|
||||||
|
// Drag-and-drop tab reordering
|
||||||
|
const draggedIndex = ref(-1);
|
||||||
|
const dragOverIndex = ref(-1);
|
||||||
|
|
||||||
|
function onDragStart(index: number, event: DragEvent): void {
|
||||||
|
draggedIndex.value = index;
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(index: number): void {
|
||||||
|
if (draggedIndex.value !== -1 && draggedIndex.value !== index) {
|
||||||
|
dragOverIndex.value = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(toIndex: number): void {
|
||||||
|
if (draggedIndex.value !== -1 && draggedIndex.value !== toIndex) {
|
||||||
|
sessionStore.moveSession(draggedIndex.value, toIndex);
|
||||||
|
}
|
||||||
|
draggedIndex.value = -1;
|
||||||
|
dragOverIndex.value = -1;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get tags for a session's underlying connection. */
|
/** Get tags for a session's underlying connection. */
|
||||||
function getSessionTags(session: Session): string[] {
|
function getSessionTags(session: Session): string[] {
|
||||||
const conn = connectionStore.connections.find((c) => c.id === session.connectionId);
|
const conn = connectionStore.connections.find((c) => c.id === session.connectionId);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useConnectionStore } from "@/stores/connection.store";
|
import { useConnectionStore } from "@/stores/connection.store";
|
||||||
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
|
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export interface Session {
|
|||||||
protocol: "ssh" | "rdp";
|
protocol: "ssh" | "rdp";
|
||||||
active: boolean;
|
active: boolean;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
status: "connected" | "disconnected";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminalDimensions {
|
export interface TerminalDimensions {
|
||||||
@ -36,10 +38,30 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
|
|
||||||
const sessionCount = computed(() => sessions.value.length);
|
const sessionCount = computed(() => sessions.value.length);
|
||||||
|
|
||||||
|
// Listen for backend close/exit events to update session status
|
||||||
|
function setupStatusListeners(sessionId: string): void {
|
||||||
|
listen(`ssh:close:${sessionId}`, () => markDisconnected(sessionId));
|
||||||
|
listen(`ssh:exit:${sessionId}`, () => markDisconnected(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDisconnected(sessionId: string): void {
|
||||||
|
const session = sessions.value.find((s) => s.id === sessionId);
|
||||||
|
if (session) session.status = "disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
function activateSession(id: string): void {
|
function activateSession(id: string): void {
|
||||||
activeSessionId.value = id;
|
activeSessionId.value = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reorder sessions by moving a tab from one index to another. */
|
||||||
|
function moveSession(fromIndex: number, toIndex: number): void {
|
||||||
|
if (fromIndex === toIndex) return;
|
||||||
|
if (fromIndex < 0 || toIndex < 0) return;
|
||||||
|
if (fromIndex >= sessions.value.length || toIndex >= sessions.value.length) return;
|
||||||
|
const [moved] = sessions.value.splice(fromIndex, 1);
|
||||||
|
sessions.value.splice(toIndex, 0, moved);
|
||||||
|
}
|
||||||
|
|
||||||
async function closeSession(id: string): Promise<void> {
|
async function closeSession(id: string): Promise<void> {
|
||||||
const idx = sessions.value.findIndex((s) => s.id === id);
|
const idx = sessions.value.findIndex((s) => s.id === id);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
@ -130,7 +152,9 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
protocol: "ssh",
|
protocol: "ssh",
|
||||||
active: true,
|
active: true,
|
||||||
username: resolvedUsername,
|
username: resolvedUsername,
|
||||||
|
status: "connected",
|
||||||
});
|
});
|
||||||
|
setupStatusListeners(sessionId);
|
||||||
activeSessionId.value = sessionId;
|
activeSessionId.value = sessionId;
|
||||||
return; // early return — key auth handled
|
return; // early return — key auth handled
|
||||||
} else {
|
} else {
|
||||||
@ -192,7 +216,9 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
protocol: "ssh",
|
protocol: "ssh",
|
||||||
active: true,
|
active: true,
|
||||||
username: resolvedUsername,
|
username: resolvedUsername,
|
||||||
|
status: "connected",
|
||||||
});
|
});
|
||||||
|
setupStatusListeners(sessionId);
|
||||||
activeSessionId.value = sessionId;
|
activeSessionId.value = sessionId;
|
||||||
} else if (conn.protocol === "rdp") {
|
} else if (conn.protocol === "rdp") {
|
||||||
let username = "";
|
let username = "";
|
||||||
@ -274,6 +300,7 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
protocol: "rdp",
|
protocol: "rdp",
|
||||||
active: true,
|
active: true,
|
||||||
username,
|
username,
|
||||||
|
status: "connected",
|
||||||
});
|
});
|
||||||
activeSessionId.value = sessionId;
|
activeSessionId.value = sessionId;
|
||||||
}
|
}
|
||||||
@ -317,6 +344,7 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
activateSession,
|
activateSession,
|
||||||
closeSession,
|
closeSession,
|
||||||
connect,
|
connect,
|
||||||
|
moveSession,
|
||||||
setTheme,
|
setTheme,
|
||||||
setTerminalDimensions,
|
setTerminalDimensions,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user