feat: ✨ sort hosts by alphabetically or last connected
This commit is contained in:
@@ -217,6 +217,22 @@ def _ensure_inline_identity_schema(cur) -> None:
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"ALTER TABLE ssh_hosts ADD COLUMN inline_identity_encrypted_key_passphrase TEXT NULL"
|
"ALTER TABLE ssh_hosts ADD COLUMN inline_identity_encrypted_key_passphrase TEXT NULL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check and add last_connected_at column
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'ssh_hosts'
|
||||||
|
AND COLUMN_NAME = 'last_connected_at'
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
if cur.fetchone() is None:
|
||||||
|
cur.execute(
|
||||||
|
"ALTER TABLE ssh_hosts ADD COLUMN last_connected_at TIMESTAMP NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _like_escape(s: str) -> str:
|
def _like_escape(s: str) -> str:
|
||||||
@@ -582,7 +598,13 @@ def _insert_connection_audit(host_row: dict[str, Any]) -> int | None:
|
|||||||
host_row.get("jump_host_id"),
|
host_row.get("jump_host_id"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return int(cur.lastrowid)
|
audit_id = int(cur.lastrowid)
|
||||||
|
# Update the host's last_connected_at timestamp
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE ssh_hosts SET last_connected_at = CURRENT_TIMESTAMP WHERE id = %s",
|
||||||
|
(int(host_row["id"]),),
|
||||||
|
)
|
||||||
|
return audit_id
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("failed to insert connection audit row")
|
log.exception("failed to insert connection audit row")
|
||||||
return None
|
return None
|
||||||
@@ -789,7 +811,7 @@ def delete_identity(iid: int):
|
|||||||
def _host_select_sql(extra_where: str = "") -> str:
|
def _host_select_sql(extra_where: str = "") -> str:
|
||||||
return f"""
|
return f"""
|
||||||
SELECT h.id, h.folder_id, h.label, h.hostname, h.port, h.identity_id, h.jump_host_id,
|
SELECT h.id, h.folder_id, h.label, h.hostname, h.port, h.identity_id, h.jump_host_id,
|
||||||
h.created_at, h.updated_at,
|
h.created_at, h.updated_at, h.last_connected_at,
|
||||||
COALESCE(i.label, 'One-time') AS identity_label,
|
COALESCE(i.label, 'One-time') AS identity_label,
|
||||||
COALESCE(i.auth_type, h.inline_identity_auth_type) AS identity_auth_type,
|
COALESCE(i.auth_type, h.inline_identity_auth_type) AS identity_auth_type,
|
||||||
pf.label AS folder_label,
|
pf.label AS folder_label,
|
||||||
|
|||||||
+26
-1
@@ -31,6 +31,7 @@ const searchQuery = ref("");
|
|||||||
const tabs = ref<TabItem[]>([]);
|
const tabs = ref<TabItem[]>([]);
|
||||||
const activeTabId = ref<string | null>(null);
|
const activeTabId = ref<string | null>(null);
|
||||||
const loadErr = ref("");
|
const loadErr = ref("");
|
||||||
|
const hostSortOrder = ref<"name" | "last_connected">("name");
|
||||||
/** Narrow viewports: slide-over hosts panel; md+ sidebar stays visible */
|
/** Narrow viewports: slide-over hosts panel; md+ sidebar stays visible */
|
||||||
const sidebarOpen = ref(true);
|
const sidebarOpen = ref(true);
|
||||||
|
|
||||||
@@ -110,6 +111,20 @@ function folderOptionLabel(id: number | null): string {
|
|||||||
return parts.join(" / ") || `#${id}`;
|
return parts.join(" / ") || `#${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSortedBrowseHosts(): HostRow[] {
|
||||||
|
const hosts = [...browseHosts.value];
|
||||||
|
if (hostSortOrder.value === "last_connected") {
|
||||||
|
// Sort by last_connected_at descending (most recent first), with never-connected at end
|
||||||
|
return hosts.sort((a, b) => {
|
||||||
|
const aTime = a.last_connected_at ? new Date(a.last_connected_at).getTime() : 0;
|
||||||
|
const bTime = b.last_connected_at ? new Date(b.last_connected_at).getTime() : 0;
|
||||||
|
return bTime - aTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sort alphabetically by label
|
||||||
|
return hosts.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshBrowse() {
|
async function refreshBrowse() {
|
||||||
try {
|
try {
|
||||||
const d = await api.browse(currentFolderId.value, searchQuery.value);
|
const d = await api.browse(currentFolderId.value, searchQuery.value);
|
||||||
@@ -599,9 +614,19 @@ async function deleteIdentityRow(id: number) {
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div v-if="browseHosts.length" class="mb-2 flex items-center justify-between">
|
||||||
|
<label class="text-xs text-slate-500 uppercase">Sort by:</label>
|
||||||
|
<select
|
||||||
|
v-model="hostSortOrder"
|
||||||
|
class="rounded border border-slate-700 bg-surface-overlay px-2 py-1 text-xs"
|
||||||
|
>
|
||||||
|
<option value="name">Name</option>
|
||||||
|
<option value="last_connected">Last connected</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li
|
<li
|
||||||
v-for="h in browseHosts"
|
v-for="h in getSortedBrowseHosts()"
|
||||||
:key="'h' + h.id"
|
:key="'h' + h.id"
|
||||||
class="rounded-lg border border-slate-800 bg-surface-overlay p-2"
|
class="rounded-lg border border-slate-800 bg-surface-overlay p-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ export interface HostRow {
|
|||||||
identity_label: string;
|
identity_label: string;
|
||||||
identity_auth_type: string;
|
identity_auth_type: string;
|
||||||
folder_label?: string | null;
|
folder_label?: string | null;
|
||||||
|
last_connected_at?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IdentityRow {
|
export interface IdentityRow {
|
||||||
|
|||||||
Reference in New Issue
Block a user