update
This commit is contained in:
@@ -218,6 +218,26 @@ export const deleteConnection = async (id: number): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定连接的 last_connected_at 时间戳
|
||||||
|
* @param id 连接 ID
|
||||||
|
* @param timestamp Unix 时间戳 (秒)
|
||||||
|
*/
|
||||||
|
export const updateLastConnected = async (id: number, timestamp: number): Promise<boolean> => {
|
||||||
|
const sql = `UPDATE connections SET last_connected_at = ? WHERE id = ?`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const result = await runDb(db, sql, [timestamp, id]);
|
||||||
|
if (result.changes === 0) {
|
||||||
|
console.warn(`[Repository] updateLastConnected: No connection found with ID ${id} to update.`);
|
||||||
|
}
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`Repository: 更新连接 ${id} 的 last_connected_at 时出错:`, err.message);
|
||||||
|
throw new Error('更新上次连接时间失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新连接的标签关联 (使用事务)
|
* 更新连接的标签关联 (使用事务)
|
||||||
* @param connectionId 连接 ID
|
* @param connectionId 连接 ID
|
||||||
|
|||||||
@@ -119,9 +119,21 @@ export const establishSshConnection = (
|
|||||||
keepaliveCountMax: 10,
|
keepaliveCountMax: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
const readyHandler = () => {
|
const readyHandler = async () => { // 改为 async 函数
|
||||||
console.log(`SshService: SSH 连接到 ${connDetails.host}:${connDetails.port} (ID: ${connDetails.id}) 成功。`);
|
console.log(`SshService: SSH 连接到 ${connDetails.host}:${connDetails.port} (ID: ${connDetails.id}) 成功。`);
|
||||||
sshClient.removeListener('error', errorHandler); // 成功后移除错误监听器
|
sshClient.removeListener('error', errorHandler); // 成功后移除错误监听器
|
||||||
|
|
||||||
|
// --- 新增:更新 last_connected_at ---
|
||||||
|
try {
|
||||||
|
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
||||||
|
await ConnectionRepository.updateLastConnected(connDetails.id, currentTimeSeconds);
|
||||||
|
console.log(`SshService: 已更新连接 ${connDetails.id} 的 last_connected_at 为 ${currentTimeSeconds}`);
|
||||||
|
} catch (updateError) {
|
||||||
|
// 更新失败不应阻止连接成功,但需要记录错误
|
||||||
|
console.error(`SshService: 更新连接 ${connDetails.id} 的 last_connected_at 失败:`, updateError);
|
||||||
|
}
|
||||||
|
// --- 结束新增 ---
|
||||||
|
|
||||||
resolve(sshClient); // 返回 Client 实例
|
resolve(sshClient); // 返回 Client 实例
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,28 +11,41 @@ export const useAuditLogStore = defineStore('auditLog', () => {
|
|||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const logsPerPage = ref(50); // Default page size
|
const logsPerPage = ref(50); // Default page size
|
||||||
|
|
||||||
// Updated fetchLogs to accept searchTerm and actionType directly
|
// fetchLogs 现在接受一个选项对象作为参数
|
||||||
const fetchLogs = async (
|
const fetchLogs = async (options: {
|
||||||
page: number = 1,
|
page?: number;
|
||||||
searchTerm?: string,
|
limit?: number; // 新增 limit 参数
|
||||||
actionType?: AuditLogActionType | '' // Allow empty string from select
|
searchTerm?: string;
|
||||||
) => {
|
actionType?: AuditLogActionType | '';
|
||||||
|
sortOrder?: 'asc' | 'desc'; // 新增 sortOrder 参数
|
||||||
|
} = {}) => {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = logsPerPage.value, // 优先使用传入的 limit,否则使用 store 的默认值
|
||||||
|
searchTerm,
|
||||||
|
actionType,
|
||||||
|
sortOrder
|
||||||
|
} = options;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
currentPage.value = page;
|
currentPage.value = page; // 仍然更新当前页码状态
|
||||||
const offset = (page - 1) * logsPerPage.value;
|
const offset = (page - 1) * limit; // offset 计算基于实际使用的 limit
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
limit: logsPerPage.value,
|
limit: limit, // 使用实际的 limit
|
||||||
offset: offset,
|
offset: offset,
|
||||||
// Add new filter parameters if they have values
|
// 条件性添加其他参数
|
||||||
...(searchTerm && { search: searchTerm }),
|
...(searchTerm && { search: searchTerm }),
|
||||||
...(actionType && { action_type: actionType }),
|
...(actionType && { action_type: actionType }),
|
||||||
|
...(sortOrder && { sort_order: sortOrder }), // 添加 sort_order 参数
|
||||||
};
|
};
|
||||||
// No need to remove undefined keys here as we conditionally add them
|
|
||||||
|
|
||||||
const response = await apiClient.get<AuditLogApiResponse>('/audit-logs', { params }); // 使用 apiClient
|
const response = await apiClient.get<AuditLogApiResponse>('/audit-logs', { params }); // 使用 apiClient
|
||||||
|
// 注意:如果 fetchLogs 被用于分页,这里直接赋值 logs.value 可能不是最佳实践
|
||||||
|
// 但对于仪表盘只获取少量最新日志的场景是可行的。
|
||||||
|
// 如果需要支持加载更多,需要修改这里的逻辑为追加或替换。
|
||||||
logs.value = response.data.logs;
|
logs.value = response.data.logs;
|
||||||
totalLogs.value = response.data.total;
|
totalLogs.value = response.data.total;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -48,7 +61,7 @@ export const useAuditLogStore = defineStore('auditLog', () => {
|
|||||||
// Function to change page size and refetch
|
// Function to change page size and refetch
|
||||||
const setLogsPerPage = (size: number) => {
|
const setLogsPerPage = (size: number) => {
|
||||||
logsPerPage.value = size;
|
logsPerPage.value = size;
|
||||||
fetchLogs(1); // Reset to first page when size changes
|
fetchLogs({ page: 1 }); // 重置到第一页,使用默认 limit
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -142,11 +142,11 @@ const totalPages = computed(() => Math.ceil(totalLogs.value / logsPerPage.value)
|
|||||||
// Function to apply filters and fetch logs
|
// Function to apply filters and fetch logs
|
||||||
const applyFilters = () => {
|
const applyFilters = () => {
|
||||||
// Pass undefined if filter is empty, otherwise pass the value
|
// Pass undefined if filter is empty, otherwise pass the value
|
||||||
store.fetchLogs(
|
store.fetchLogs({
|
||||||
1, // Reset to page 1 when applying filters
|
page: 1, // Reset to page 1 when applying filters
|
||||||
searchTerm.value || undefined,
|
searchTerm: searchTerm.value || undefined,
|
||||||
selectedActionType.value || undefined
|
actionType: selectedActionType.value || undefined
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Removed watch for filters
|
// Removed watch for filters
|
||||||
@@ -182,7 +182,12 @@ const formatDetails = (details: AuditLogEntry['details']): string => {
|
|||||||
|
|
||||||
const changePage = (page: number) => {
|
const changePage = (page: number) => {
|
||||||
if (page >= 1 && page <= totalPages.value && page !== currentPage.value) {
|
if (page >= 1 && page <= totalPages.value && page !== currentPage.value) {
|
||||||
store.fetchLogs(page);
|
// Retain current filters when changing page
|
||||||
|
store.fetchLogs({
|
||||||
|
page: page,
|
||||||
|
searchTerm: searchTerm.value || undefined,
|
||||||
|
actionType: selectedActionType.value || undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,25 @@ const maxRecentLogs = 5;
|
|||||||
|
|
||||||
// --- 最近连接 ---
|
// --- 最近连接 ---
|
||||||
const recentConnections = computed(() => {
|
const recentConnections = computed(() => {
|
||||||
// 过滤掉 last_connected_at 为 null 或 undefined 的连接
|
console.log('[Dashboard] Raw connections from store:', JSON.parse(JSON.stringify(connections.value)));
|
||||||
const connected = connections.value.filter(c => c.last_connected_at); // 使用 last_connected_at
|
|
||||||
// 按 last_connected_at 降序排序
|
// 优先尝试按 last_connected_at 过滤和排序
|
||||||
connected.sort((a, b) => (b.last_connected_at ?? 0) - (a.last_connected_at ?? 0)); // 使用 last_connected_at
|
const connected = connections.value.filter(c => c.last_connected_at);
|
||||||
// 取前 N 条
|
console.log('[Dashboard] Filtered connections (with last_connected_at):', JSON.parse(JSON.stringify(connected)));
|
||||||
return connected.slice(0, maxRecentConnections);
|
|
||||||
|
if (connected.length > 0) {
|
||||||
|
connected.sort((a, b) => (b.last_connected_at ?? 0) - (a.last_connected_at ?? 0));
|
||||||
|
const result = connected.slice(0, maxRecentConnections);
|
||||||
|
console.log('[Dashboard] Final recent connections (using last_connected_at):', JSON.parse(JSON.stringify(result)));
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// 如果没有带 last_connected_at 的连接,则按 updated_at 排序显示最近更新的
|
||||||
|
console.log('[Dashboard] No connections with last_connected_at found. Falling back to sorting by updated_at.');
|
||||||
|
const sortedByUpdate = [...connections.value].sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
|
||||||
|
const result = sortedByUpdate.slice(0, maxRecentConnections);
|
||||||
|
console.log('[Dashboard] Final recent connections (fallback using updated_at):', JSON.parse(JSON.stringify(result)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 最近活动 ---
|
// --- 最近活动 ---
|
||||||
@@ -42,7 +55,9 @@ onMounted(async () => {
|
|||||||
// 如果 connections store 还没有加载过数据,则加载
|
// 如果 connections store 还没有加载过数据,则加载
|
||||||
if (connections.value.length === 0) {
|
if (connections.value.length === 0) {
|
||||||
try {
|
try {
|
||||||
|
console.log('[Dashboard] onMounted: Fetching connections...'); // 添加日志
|
||||||
await connectionsStore.fetchConnections();
|
await connectionsStore.fetchConnections();
|
||||||
|
console.log('[Dashboard] onMounted: Connections fetched.'); // 添加日志
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("加载连接列表失败:", error);
|
console.error("加载连接列表失败:", error);
|
||||||
// 可以在这里显示错误通知
|
// 可以在这里显示错误通知
|
||||||
@@ -50,8 +65,12 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
// 加载最新的审计日志
|
// 加载最新的审计日志
|
||||||
try {
|
try {
|
||||||
// 只需要加载少量日志用于摘要
|
// 只需要加载少量日志用于摘要,并按时间倒序
|
||||||
await auditLogStore.fetchLogs(1, maxRecentLogs, '', 'desc'); // 使用修正后的变量名
|
await auditLogStore.fetchLogs({
|
||||||
|
page: 1,
|
||||||
|
limit: maxRecentLogs, // 传递 limit
|
||||||
|
sortOrder: 'desc' // 传递 sortOrder
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("加载审计日志失败:", error);
|
console.error("加载审计日志失败:", error);
|
||||||
// 可以在这里显示错误通知
|
// 可以在这里显示错误通知
|
||||||
@@ -70,7 +89,14 @@ const connectTo = (connection: ConnectionBase) => { // 使用 ConnectionBase 类
|
|||||||
const formatRelativeTime = (dateString: string | undefined | null): string => {
|
const formatRelativeTime = (dateString: string | undefined | null): string => {
|
||||||
if (!dateString) return t('connections.status.never');
|
if (!dateString) return t('connections.status.never');
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
// 将秒级时间戳转换为毫秒级
|
||||||
|
const timestampInMs = Number(dateString) * 1000;
|
||||||
|
// 检查转换后的值是否有效
|
||||||
|
if (isNaN(timestampInMs)) {
|
||||||
|
console.warn(`[Dashboard] Invalid timestamp received: ${dateString}`);
|
||||||
|
return dateString; // 返回原始值或错误提示
|
||||||
|
}
|
||||||
|
const date = new Date(timestampInMs);
|
||||||
const currentLocale = locale.value === 'zh' ? zhCN : enUS;
|
const currentLocale = locale.value === 'zh' ? zhCN : enUS;
|
||||||
return formatDistanceToNow(date, { addSuffix: true, locale: currentLocale });
|
return formatDistanceToNow(date, { addSuffix: true, locale: currentLocale });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user