This commit is contained in:
Baobhan Sith
2025-04-23 20:35:50 +08:00
parent fb7a5374ae
commit f7fe1904cf
3 changed files with 364 additions and 333 deletions
@@ -1,342 +1,39 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
// import apiClient from '../utils/apiClient'; // Removed apiClient
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSessionStore } from '../stores/session.store'; // Import session store
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '../stores/settings.store'; // +++ Import settings store +++
// Removed unused imports: ref, onMounted, onUnmounted, watch, useSettingsStore
// Removed unused interfaces: PortInfo, DockerContainer, DockerStats (now in composable)
const { t } = useI18n();
const sessionStore = useSessionStore();
const { activeSession } = storeToRefs(sessionStore); // Get reactive active session
const settingsStore = useSettingsStore(); // +++ Get settings store instance +++
const { dockerDefaultExpandBoolean } = storeToRefs(settingsStore); // +++ Get reactive getter +++
// --- Interfaces ---
interface PortInfo {
IP?: string;
PrivatePort: number;
PublicPort?: number;
Type: 'tcp' | 'udp' | string;
}
// --- Get Docker Manager Instance from Active Session ---
const dockerManager = computed(() => activeSession.value?.dockerManager);
// --- Interfaces ---
interface PortInfo {
IP?: string;
PrivatePort: number;
PublicPort?: number;
Type: 'tcp' | 'udp' | string;
}
// --- Computed properties based on Docker Manager state ---
const containers = computed(() => dockerManager.value?.containers.value ?? []);
const isLoading = computed(() => dockerManager.value?.isLoading.value ?? false);
const error = computed(() => dockerManager.value?.error.value ?? null);
const isDockerAvailable = computed(() => dockerManager.value?.isDockerAvailable.value ?? false); // Default to false if no manager
const expandedContainerIds = computed(() => dockerManager.value?.expandedContainerIds.value ?? new Set<string>());
interface DockerContainer {
id: string; // <--- Changed from Id to id
Names: string[];
Image: string;
ImageID: string;
Command: string;
Created: number;
State: 'created' | 'restarting' | 'running' | 'removing' | 'paused' | 'exited' | 'dead' | string;
Status: string;
Ports: PortInfo[];
Labels: Record<string, string>;
stats?: DockerStats | null; // ADDED: Assume stats are pushed with the container data
}
// --- NEW: Stats Interface (Example structure, adjust based on actual docker stats json output) ---
interface DockerStats {
ID: string;
Name: string;
CPUPerc: string; // e.g., "0.07%"
MemUsage: string; // e.g., "100MiB / 1.95GiB"
MemPerc: string; // e.g., "5.00%"
NetIO: string; // e.g., "1.5kB / 648B"
BlockIO: string; // e.g., "10MB / 0B"
PIDs: string; // e.g., "10"
// Add other fields if available from `docker stats --format json`
}
// --- State ---
const containers = ref<DockerContainer[]>([]);
const isLoading = ref(false);
const error = ref<string | null>(null);
const isDockerAvailable = ref(true); // This will now reflect remote docker availability
let refreshInterval: ReturnType<typeof setInterval> | null = null;
// REMOVED: statsRefreshInterval
let wsUnsubscribeHooks: (() => void)[] = []; // To store unsubscribe functions
// --- State for expansion (multiple allowed) ---
const expandedContainerIds = ref<Set<string>>(new Set()); // Use a Set to store multiple IDs
const initialLoadDone = ref(false); // +++ Flag for initial load processing +++
// REMOVED: containerStats, isStatsLoading, statsError maps
// --- Computed ---
// --- Computed properties for UI state (independent of dockerManager) ---
const currentSessionId = computed(() => activeSession.value?.sessionId);
// Add computed property for SSH connection status
const sshConnectionStatus = computed(() => activeSession.value?.wsManager.connectionStatus.value ?? 'disconnected');
// --- Methods ---
// Clear existing WebSocket listeners
const clearWsListeners = () => {
wsUnsubscribeHooks.forEach(unsub => unsub());
wsUnsubscribeHooks = [];
};
// Setup WebSocket listeners
const setupWsListeners = () => {
clearWsListeners(); // Clear previous listeners first
if (!activeSession.value) return;
const wsManager = activeSession.value.wsManager;
// Listener for Docker status updates
// Listener for Docker status updates (SIMPLIFIED)
const unsubStatus = wsManager.onMessage('docker:status:update', (payload) => {
console.log('[DockerManager] Received docker:status:update', payload);
isLoading.value = false; // Stop loading indicator
if (payload && typeof payload.available === 'boolean') {
isDockerAvailable.value = payload.available;
if (payload.available && Array.isArray(payload.containers)) {
// Directly replace the containers list with the received data
// Assuming payload.containers includes the 'stats' property for each container
containers.value = payload.containers as DockerContainer[];
error.value = null;
// Clean up expansion state for containers that no longer exist
const currentIds = new Set(containers.value.map(c => c.id));
const idsToRemove = new Set<string>();
expandedContainerIds.value.forEach(id => {
if (!currentIds.has(id)) {
idsToRemove.add(id);
}
});
idsToRemove.forEach(id => expandedContainerIds.value.delete(id));
// +++ Handle default expand on initial load +++
if (!initialLoadDone.value && dockerDefaultExpandBoolean.value) {
console.log('[DockerManager] Applying default expand setting.');
containers.value.forEach(container => {
if (!expandedContainerIds.value.has(container.id)) {
expandedContainerIds.value.add(container.id);
}
});
initialLoadDone.value = true; // Mark initial load processed
}
// +++ End handle default expand +++
} else {
// Docker available but no containers, or Docker unavailable
containers.value = [];
error.value = null;
expandedContainerIds.value.clear(); // Collapse all
// Stop main refresh interval if Docker becomes unavailable remotely
// (No stats interval to stop anymore)
if (refreshInterval && !payload.available) {
clearInterval(refreshInterval);
refreshInterval = null;
console.log('[DockerManager] Stopped refresh interval due to remote Docker unavailability.');
}
}
} else {
// Handle invalid payload
isDockerAvailable.value = false;
containers.value = [];
error.value = t('dockerManager.error.invalidResponse');
expandedContainerIds.value.clear(); // Collapse all
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = null;
// No stats interval to stop
}
});
// Listener for Docker status fetch errors
const unsubStatusError = wsManager.onMessage('docker:status:error', (payload) => {
console.error('[DockerManager] Received docker:status:error', payload);
isLoading.value = false;
error.value = payload?.message || t('dockerManager.error.fetchFailed');
isDockerAvailable.value = false; // Assume unavailable on error
containers.value = [];
if (refreshInterval) clearInterval(refreshInterval); // Stop interval on error
refreshInterval = null;
});
// Listener for Docker command execution errors (optional, could use notifications)
const unsubCommandError = wsManager.onMessage('docker:command:error', (payload) => {
console.error('[DockerManager] Received docker:command:error', payload);
// Display error to user (e.g., using a notification system)
alert(`${t('dockerManager.error.commandFailed', { command: payload?.command || '?' })}: ${payload?.message || 'Unknown error'}`);
});
// --- NEW: Listener for backend requesting a status update ---
const unsubRequestUpdate = wsManager.onMessage('request_docker_status_update', () => {
console.log('[DockerManager] Received request_docker_status_update from backend.');
// Debounce or add slight delay? Maybe not needed if backend delay is sufficient.
requestDockerStatus(); // Trigger a status refresh immediately
});
// REMOVED: unsubStatsUpdate and unsubStatsError listeners
wsUnsubscribeHooks.push(
unsubStatus,
unsubStatusError,
unsubCommandError,
unsubRequestUpdate
);
};
// Request Docker status via WebSocket - NOW CHECKS SSH STATUS
const requestDockerStatus = () => {
// Only request if SSH is connected
if (sshConnectionStatus.value !== 'connected') {
console.log(`[DockerManager] SSH not connected (status: ${sshConnectionStatus.value}), skipping Docker status request.`);
// No need to set loading=false here, as it should only be set true when connected
return;
}
if (!activeSession.value) { // Should not happen if ssh is connected, but for safety
console.warn('[DockerManager] requestDockerStatus called without active session.');
return;
}
console.log(`[DockerManager] Requesting Docker status for session ${activeSession.value.sessionId}`);
isLoading.value = true; // Show loading indicator
error.value = null; // Clear previous error
activeSession.value.wsManager.sendMessage({ type: 'docker:get_status' });
};
// Send command for a specific container via WebSocket
// --- Methods delegated to Docker Manager ---
const sendDockerCommand = (containerId: string, command: 'start' | 'stop' | 'restart' | 'remove') => {
// Check SSH status first
if (sshConnectionStatus.value !== 'connected') {
console.warn('[DockerManager] Cannot send command, SSH not connected.');
alert(t('dockerManager.error.sshNotConnected')); // Inform user
return;
}
if (!activeSession.value) { // Safety check
console.warn('[DockerManager] Cannot send command, no active session.');
return;
}
if (!isDockerAvailable.value) {
console.warn('[DockerManager] Cannot send command, remote Docker is not available.');
alert(t('dockerManager.notAvailable'));
return;
}
console.log(`[DockerManager] Sending command '${command}' for container ${containerId} via session ${activeSession.value.sessionId}`);
activeSession.value.wsManager.sendMessage({
type: 'docker:command',
payload: { containerId, command }
});
// Optionally trigger a status refresh sooner after a command
// setTimeout(requestDockerStatus, 500); // e.g., refresh after 0.5s
// --- 添加日志,检查传入的 containerId ---
console.log(`[DockerManager] Preparing to send command. containerId: "${containerId}" (Type: ${typeof containerId}), command: "${command}"`);
// --- 结束日志 ---
console.log(`[DockerManager] Sending command '${command}' for container ${containerId} via session ${activeSession.value.sessionId}`);
activeSession.value.wsManager.sendMessage({
type: 'docker:command',
payload: { containerId, command }
});
dockerManager.value?.sendDockerCommand(containerId, command);
};
// --- SIMPLIFIED: Method to toggle expansion ---
const toggleExpand = (containerId: string) => {
if (expandedContainerIds.value.has(containerId)) {
expandedContainerIds.value.delete(containerId);
console.log(`[DockerManager] Collapsed container ${containerId}.`);
} else {
expandedContainerIds.value.add(containerId);
console.log(`[DockerManager] Expanded container ${containerId}.`);
// No need to request stats here, they should be in containers.value
}
dockerManager.value?.toggleExpand(containerId);
};
// REMOVED: requestExpandedStats function
// --- Lifecycle and Watchers ---
// --- SIMPLIFIED Watcher ---
watch([currentSessionId, sshConnectionStatus], ([newSessionId, newSshStatus], [oldSessionId, oldSshStatus]) => {
console.log(`[DockerManager] Watch triggered. Session: ${oldSessionId}=>${newSessionId}, SSH Status: ${oldSshStatus}=>${newSshStatus}`);
// --- Clear state and main interval on session change or SSH disconnect/error ---
const resetStateAndInterval = () => {
console.log('[DockerManager] Resetting state and clearing main interval.');
containers.value = [];
isLoading.value = false;
error.value = null;
isDockerAvailable.value = true; // Assume available until fetch attempt
expandedContainerIds.value.clear(); // Clear expansion state
initialLoadDone.value = false; // +++ Reset initial load flag +++
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
console.log('[DockerManager] Cleared main refresh interval.');
}
// No stats interval to clear
clearWsListeners(); // Clear listeners
};
if (newSessionId !== oldSessionId || (newSessionId && (newSshStatus === 'disconnected' || newSshStatus === 'error'))) {
resetStateAndInterval();
}
// --- Setup listeners and start main interval when session is active AND SSH is connected ---
if (newSessionId && newSshStatus === 'connected') {
// Only setup/start if we weren't already connected or interval isn't running
if (oldSshStatus !== 'connected' || newSessionId !== oldSessionId || !refreshInterval) {
console.log(`[DockerManager] Session ${newSessionId} connected. Setting up listeners and starting main interval.`);
setupWsListeners();
requestDockerStatus(); // Fetch initial status
// Start main status refresh interval (if backend doesn't push automatically)
// If backend *does* push automatically, this interval might be redundant or cause extra load.
// Consider making the interval optional or configurable based on backend behavior.
if (!refreshInterval) {
// Let's keep a slower interval for safety, maybe backend push fails sometimes
refreshInterval = setInterval(requestDockerStatus, 15000); // Check status every 15 seconds
console.log('[DockerManager] Main refresh interval started (15s).');
}
// No stats interval to start
}
} else if (newSessionId && newSshStatus === 'connecting') {
isLoading.value = true;
error.value = null;
containers.value = [];
isDockerAvailable.value = false;
console.log('[DockerManager] SSH is connecting, waiting...');
// Ensure main interval is stopped while connecting
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = null;
} else {
// Handle cases like no active session or other statuses
isLoading.value = false;
console.log('[DockerManager] No active session or SSH not connected/connecting.');
// Ensure main interval is stopped if not connected
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = null;
}
}, { immediate: true });
// --- SIMPLIFIED onUnmounted ---
onUnmounted(() => {
console.log('[DockerManager] Component unmounted.');
clearWsListeners(); // Clean up listeners
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
console.log('[DockerManager] Main refresh interval cleared on unmount.');
}
// No stats interval to clear
});
// --- Removed internal state, methods (setupWsListeners, clearWsListeners, requestDockerStatus), watcher, and lifecycle hooks (onMounted, onUnmounted) ---
</script>
@@ -367,27 +64,27 @@ onUnmounted(() => {
<small class="text-xs max-w-[80%]">{{ activeSession?.wsManager.statusMessage.value || 'Unknown SSH error' }}</small>
</div>
<!-- Case 5: Active session, SSH connected, Docker loading -->
<div v-else-if="isLoading && containers.length === 0" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary p-4">
<div v-else-if="isLoading && containers.length === 0" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary p-4"> <!-- Use computed isLoading -->
<i class="fas fa-spinner fa-spin text-4xl mb-3"></i> {{ t('dockerManager.loading') }}
</div>
<!-- Case 6: Active session, SSH connected, Docker unavailable -->
<div v-else-if="!isDockerAvailable" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary p-4">
<div v-else-if="!isDockerAvailable" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary p-4"> <!-- Use computed isDockerAvailable -->
<i class="fab fa-docker text-4xl mb-3"></i>
<p class="mt-2 mb-1 font-medium">{{ t('dockerManager.notAvailable') }}</p>
<small class="text-xs max-w-[80%] text-text-disabled">{{ t('dockerManager.installHintRemote') }}</small>
</div>
<!-- Case 7: Active session, SSH connected, Fetch error -->
<div v-else-if="error" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary p-4">
<div v-else-if="error" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary p-4"> <!-- Use computed error -->
<i class="fas fa-exclamation-triangle text-3xl text-red-500 mb-2"></i>
<p class="mt-2 mb-1 font-medium">{{ t('dockerManager.error.fetchFailed') }}</p>
<small class="text-xs max-w-[80%]">{{ error }}</small>
<small class="text-xs max-w-[80%]">{{ error }}</small> <!-- Use computed error -->
</div>
<!-- Case 8: Active session, SSH connected, Docker available, show list -->
<div v-else class="docker-content-area flex-grow overflow-auto">
<div v-if="containers.length === 0 && !isLoading" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary h-full">
<div v-else class="docker-content-area flex-grow overflow-auto"> <!-- This 'else' covers the 'connected and available' case -->
<div v-if="containers.length === 0 && !isLoading" class="flex flex-col justify-center items-center text-center flex-grow text-text-secondary h-full"> <!-- Use computed containers and isLoading -->
{{ t('dockerManager.noContainers') }}
</div>
<table v-else class="w-full border-collapse text-sm">
<table v-else class="w-full border-collapse text-sm"> <!-- Use computed containers -->
<thead class="responsive-thead"> <!-- Use class for CSS control -->
<tr class="bg-header">
<th class="w-8 px-2 py-2 border-b border-border"></th> <!-- Expand Col -->
@@ -400,14 +97,14 @@ onUnmounted(() => {
</thead>
<!-- Use template v-for to render pairs of rows -->
<tbody class="responsive-tbody"> <!-- Use class for CSS control -->
<template v-for="container in containers" :key="container.id">
<template v-for="container in containers" :key="container.id"> <!-- Use computed containers -->
<!-- Main Row / Card Container -->
<tr class="responsive-tr mb-4 border border-border rounded p-3 bg-background shadow-sm relative hover:bg-header/30 transition-colors duration-150"
:class="{'expanded': expandedContainerIds.has(container.id)}">
:class="{'expanded': expandedContainerIds.has(container.id)}"> <!-- Use computed expandedContainerIds -->
<!-- Expand Button Cell (Desktop only) -->
<td class="responsive-td-expand w-8 px-2 py-2 border-b border-border text-center align-middle">
<button @click="toggleExpand(container.id)" class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 text-xs" :title="expandedContainerIds.has(container.id) ? t('common.collapse') : t('common.expand')">
<i :class="['fas', expandedContainerIds.has(container.id) ? 'fa-chevron-down' : 'fa-chevron-right']"></i>
<button @click="toggleExpand(container.id)" class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 text-xs" :title="expandedContainerIds.has(container.id) ? t('common.collapse') : t('common.expand')"> <!-- Use computed expandedContainerIds -->
<i :class="['fas', expandedContainerIds.has(container.id) ? 'fa-chevron-down' : 'fa-chevron-right']"></i> <!-- Use computed expandedContainerIds -->
</button>
</td>
<!-- Name Cell -->
@@ -454,13 +151,13 @@ onUnmounted(() => {
<!-- Card Expansion Cell (Mobile only) -->
<td class="responsive-td-card-expand w-full p-0 border-t border-border mt-3">
<!-- Card Footer Button (Show when NOT expanded) -->
<div v-if="!expandedContainerIds.has(container.id)">
<div v-if="!expandedContainerIds.has(container.id)"> <!-- Use computed expandedContainerIds -->
<button @click="toggleExpand(container.id)" class="flex items-center justify-center w-full h-10 text-text-secondary hover:text-foreground hover:bg-header/50 transition-colors duration-150 text-sm rounded-b">
<i class="fas fa-chevron-down mr-1.5"></i> {{ t('common.expand') }}
</button>
</div>
<!-- Card Expansion Content (Show when expanded) -->
<div v-if="expandedContainerIds.has(container.id)" class="bg-header/30 rounded-b">
<div v-if="expandedContainerIds.has(container.id)" class="bg-header/30 rounded-b"> <!-- Use computed expandedContainerIds -->
<div class="p-4"> <!-- Stats Container -->
<dl v-if="container.stats" class="grid grid-cols-[max-content_auto] gap-x-4 gap-y-2 text-xs">
<dt class="font-medium text-text-secondary">{{ t('dockerManager.stats.cpu') }}</dt>
@@ -487,7 +184,7 @@ onUnmounted(() => {
</tr>
<!-- Desktop Expansion Row (Hidden on mobile) -->
<tr v-if="expandedContainerIds.has(container.id)" class="responsive-expansion-row">
<tr v-if="expandedContainerIds.has(container.id)" class="responsive-expansion-row"> <!-- Use computed expandedContainerIds -->
<td :colspan="6" class="p-0 border-b border-border">
<div class="bg-header/30 p-4"> <!-- Stats Container -->
<dl v-if="container.stats" class="grid grid-cols-[max-content_auto] gap-x-4 gap-y-2 text-xs">
@@ -0,0 +1,320 @@
import { ref, readonly, watch, computed, type Ref, type ComputedRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '../stores/settings.store';
import { storeToRefs } from 'pinia';
import type { WebSocketMessage, MessagePayload } from '../types/websocket.types';
// --- Interfaces (Copied from DockerManager.vue) ---
interface PortInfo {
IP?: string;
PrivatePort: number;
PublicPort?: number;
Type: 'tcp' | 'udp' | string;
}
export interface DockerContainer { // Exporting for potential use elsewhere
id: string;
Names: string[];
Image: string;
ImageID: string;
Command: string;
Created: number;
State: 'created' | 'restarting' | 'running' | 'removing' | 'paused' | 'exited' | 'dead' | string;
Status: string;
Ports: PortInfo[];
Labels: Record<string, string>;
stats?: DockerStats | null;
}
export interface DockerStats { // Exporting for potential use elsewhere
ID: string;
Name: string;
CPUPerc: string;
MemUsage: string;
MemPerc: string;
NetIO: string;
BlockIO: string;
PIDs: string;
}
// --- WebSocket Dependencies Interface ---
// Similar to other composables, defining dependencies for WS communication
export interface DockerManagerDependencies {
sendMessage: (message: WebSocketMessage) => void;
onMessage: (type: string, handler: (payload: any, fullMessage?: WebSocketMessage) => void) => () => void;
isConnected: ComputedRef<boolean>;
// We might need isSshReady or similar if Docker commands depend on SSH being fully ready
// For now, isConnected might suffice, assuming WS connection implies SSH readiness for Docker
}
/**
* Creates a Docker manager instance for a specific session.
* @param sessionId The unique identifier for the session.
* @param wsDeps WebSocket dependencies object.
* @param i18n The i18n instance (t function).
* @returns Docker manager instance.
*/
export function createDockerManager(sessionId: string, wsDeps: DockerManagerDependencies, i18n: { t: (key: string, params?: any) => string }) {
const { sendMessage, onMessage, isConnected } = wsDeps;
const { t } = i18n; // Use the passed i18n instance
// --- State ---
const containers = ref<DockerContainer[]>([]);
const isLoading = ref(false);
const error = ref<string | null>(null);
const isDockerAvailable = ref(true); // Assume available until checked
const expandedContainerIds = ref<Set<string>>(new Set());
const initialLoadDone = ref(false);
let refreshInterval: ReturnType<typeof setInterval> | null = null;
let wsUnsubscribeHooks: (() => void)[] = [];
// --- Settings Store ---
// Settings need to be accessed here as well for default expansion
const settingsStore = useSettingsStore();
const { dockerDefaultExpandBoolean } = storeToRefs(settingsStore);
// --- Methods ---
// Clear existing WebSocket listeners
const clearWsListeners = () => {
if (wsUnsubscribeHooks.length > 0) {
console.log(`[DockerManager ${sessionId}] Clearing ${wsUnsubscribeHooks.length} WebSocket listeners.`);
wsUnsubscribeHooks.forEach(unsub => unsub());
wsUnsubscribeHooks = [];
}
};
// Request Docker status via WebSocket
const requestDockerStatus = () => {
if (!isConnected.value) {
console.log(`[DockerManager ${sessionId}] WebSocket not connected, skipping Docker status request.`);
// Reset state if disconnected? Or rely on watch(isConnected)?
// Let's reset here for immediate feedback if called manually while disconnected.
containers.value = [];
isLoading.value = false;
error.value = t('dockerManager.error.sshDisconnected'); // Use a generic disconnected message
isDockerAvailable.value = false;
expandedContainerIds.value.clear();
initialLoadDone.value = false;
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = null;
return;
}
console.log(`[DockerManager ${sessionId}] Requesting Docker status.`);
isLoading.value = true;
error.value = null; // Clear previous error
sendMessage({ type: 'docker:get_status', sessionId }); // Ensure sessionId is included if needed by backend routing
};
// Setup WebSocket listeners
const setupWsListeners = () => {
clearWsListeners(); // Clear previous listeners first
if (!isConnected.value) {
console.warn(`[DockerManager ${sessionId}] Cannot setup listeners, WebSocket not connected.`);
return;
}
console.log(`[DockerManager ${sessionId}] Setting up WebSocket listeners.`);
const unsubStatus = onMessage('docker:status:update', (payload, message) => {
if (message?.sessionId && message.sessionId !== sessionId) return; // Ignore messages for other sessions
console.log(`[DockerManager ${sessionId}] Received docker:status:update`, payload);
isLoading.value = false;
if (payload && typeof payload.available === 'boolean') {
isDockerAvailable.value = payload.available;
if (payload.available && Array.isArray(payload.containers)) {
containers.value = payload.containers as DockerContainer[];
error.value = null;
// Clean up expansion state
const currentIds = new Set(containers.value.map(c => c.id));
const idsToRemove = new Set<string>();
expandedContainerIds.value.forEach(id => {
if (!currentIds.has(id)) idsToRemove.add(id);
});
idsToRemove.forEach(id => expandedContainerIds.value.delete(id));
// Handle default expand on initial load
if (!initialLoadDone.value && dockerDefaultExpandBoolean.value) {
console.log(`[DockerManager ${sessionId}] Applying default expand setting.`);
containers.value.forEach(container => {
if (!expandedContainerIds.value.has(container.id)) {
expandedContainerIds.value.add(container.id);
}
});
initialLoadDone.value = true;
}
} else {
containers.value = [];
error.value = null;
expandedContainerIds.value.clear();
if (refreshInterval && !payload.available) {
clearInterval(refreshInterval);
refreshInterval = null;
console.log(`[DockerManager ${sessionId}] Stopped refresh interval due to remote Docker unavailability.`);
}
}
} else {
isDockerAvailable.value = false;
containers.value = [];
error.value = t('dockerManager.error.invalidResponse');
expandedContainerIds.value.clear();
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = null;
}
});
const unsubStatusError = onMessage('docker:status:error', (payload, message) => {
if (message?.sessionId && message.sessionId !== sessionId) return;
console.error(`[DockerManager ${sessionId}] Received docker:status:error`, payload);
isLoading.value = false;
error.value = payload?.message || t('dockerManager.error.fetchFailed');
isDockerAvailable.value = false;
containers.value = [];
expandedContainerIds.value.clear();
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = null;
});
const unsubCommandError = onMessage('docker:command:error', (payload, message) => {
if (message?.sessionId && message.sessionId !== sessionId) return;
console.error(`[DockerManager ${sessionId}] Received docker:command:error`, payload);
// How to notify UI? Maybe set an error ref? Or rely on status update?
// For now, just log. UI component could show a generic error or use a notification system.
// Consider adding a transient commandError ref if needed.
alert(`${t('dockerManager.error.commandFailed', { command: payload?.command || '?' })}: ${payload?.message || 'Unknown error'}`);
});
const unsubRequestUpdate = onMessage('request_docker_status_update', (payload, message) => {
if (message?.sessionId && message.sessionId !== sessionId) return;
console.log(`[DockerManager ${sessionId}] Received request_docker_status_update from backend.`);
requestDockerStatus(); // Trigger a status refresh immediately
});
wsUnsubscribeHooks.push(unsubStatus, unsubStatusError, unsubCommandError, unsubRequestUpdate);
};
// Send command for a specific container via WebSocket
const sendDockerCommand = (containerId: string, command: 'start' | 'stop' | 'restart' | 'remove') => {
if (!isConnected.value) {
console.warn(`[DockerManager ${sessionId}] Cannot send command, WebSocket not connected.`);
alert(t('dockerManager.error.sshNotConnected')); // Use generic disconnected message
return;
}
if (!isDockerAvailable.value) {
console.warn(`[DockerManager ${sessionId}] Cannot send command, remote Docker is not available.`);
alert(t('dockerManager.notAvailable'));
return;
}
console.log(`[DockerManager ${sessionId}] Sending command '${command}' for container ${containerId}`);
sendMessage({
type: 'docker:command',
sessionId, // Include sessionId if needed by backend routing
payload: { containerId, command }
});
// Optionally trigger a status refresh sooner after a command
// setTimeout(requestDockerStatus, 500);
};
// Toggle expansion state for a container
const toggleExpand = (containerId: string) => {
if (expandedContainerIds.value.has(containerId)) {
expandedContainerIds.value.delete(containerId);
console.log(`[DockerManager ${sessionId}] Collapsed container ${containerId}.`);
} else {
expandedContainerIds.value.add(containerId);
console.log(`[DockerManager ${sessionId}] Expanded container ${containerId}.`);
}
};
// --- Lifecycle Management ---
// Reset state function
const resetStateAndInterval = () => {
console.log(`[DockerManager ${sessionId}] Resetting state and clearing interval.`);
containers.value = [];
isLoading.value = false;
error.value = null;
isDockerAvailable.value = true; // Assume available until checked
expandedContainerIds.value.clear();
initialLoadDone.value = false;
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
console.log(`[DockerManager ${sessionId}] Cleared main refresh interval.`);
}
clearWsListeners();
};
// Watch for connection changes to manage listeners and interval
watch(isConnected, (newIsConnected, oldIsConnected) => {
console.log(`[DockerManager ${sessionId}] Connection status changed: ${oldIsConnected} -> ${newIsConnected}`);
if (newIsConnected) {
// Connection established
setupWsListeners();
requestDockerStatus(); // Fetch initial status
// Start refresh interval (consider if backend pushes updates reliably)
if (!refreshInterval) {
// Keep a safety interval
refreshInterval = setInterval(requestDockerStatus, 15000); // Check every 15s
console.log(`[DockerManager ${sessionId}] Main refresh interval started (15s).`);
}
} else {
// Connection lost
resetStateAndInterval();
// Set error state to indicate disconnection
error.value = t('dockerManager.error.sshDisconnected');
isDockerAvailable.value = false; // Assume unavailable when disconnected
}
}, { immediate: false }); // Don't run immediately, let initial connect trigger it
// Cleanup function to be called when the session ends
const cleanup = () => {
console.log(`[DockerManager ${sessionId}] Cleaning up.`);
resetStateAndInterval(); // Clears listeners and interval
};
// --- Initial Setup ---
// If already connected when this manager is created, set up listeners and fetch data.
// This handles cases where the manager is created after the WS connection is live.
if (isConnected.value) {
console.log(`[DockerManager ${sessionId}] Initial setup: Already connected.`);
setupWsListeners();
requestDockerStatus();
if (!refreshInterval) {
refreshInterval = setInterval(requestDockerStatus, 15000);
console.log(`[DockerManager ${sessionId}] Initial setup: Main refresh interval started (15s).`);
}
} else {
console.log(`[DockerManager ${sessionId}] Initial setup: Not connected yet.`);
// Set initial state for disconnected status
error.value = t('dockerManager.error.sshDisconnected');
isDockerAvailable.value = false;
}
// --- Exposed Interface ---
return {
// Readonly State
containers: readonly(containers),
isLoading: readonly(isLoading),
error: readonly(error),
isDockerAvailable: readonly(isDockerAvailable),
expandedContainerIds: readonly(expandedContainerIds), // UI needs this read-only
// Methods
requestDockerStatus, // Might be useful for manual refresh button in UI
sendDockerCommand,
toggleExpand, // UI needs this to handle clicks
// Lifecycle
cleanup,
};
}
// Export the type of the returned manager instance
export type DockerManagerInstance = ReturnType<typeof createDockerManager>;
@@ -11,6 +11,7 @@ import { createWebSocketConnectionManager, type WsConnectionStatus } from '../co
import { createSftpActionsManager, type WebSocketDependencies } from '../composables/useSftpActions';
import { createSshTerminalManager, type SshTerminalDependencies } from '../composables/useSshTerminal';
import { createStatusMonitorManager, type StatusMonitorDependencies } from '../composables/useStatusMonitor';
import { createDockerManager, type DockerManagerInstance, type DockerManagerDependencies } from '../composables/useDockerManager'; // +++ Import Docker Manager +++
// --- 辅助函数 ---
function generateSessionId(): string {
@@ -55,6 +56,7 @@ export type WsManagerInstance = ReturnType<typeof createWebSocketConnectionManag
export type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
export type SshTerminalInstance = ReturnType<typeof createSshTerminalManager>;
export type StatusMonitorInstance = ReturnType<typeof createStatusMonitorManager>;
export type DockerManagerInstance = ReturnType<typeof createDockerManager>; // +++ Add Docker Manager Instance Type +++
export interface SessionState {
sessionId: string;
@@ -65,6 +67,7 @@ export interface SessionState {
sftpManagers: Map<string, SftpManagerInstance>; // 使用 Map 管理多个实例
terminalManager: SshTerminalInstance;
statusMonitorManager: StatusMonitorInstance;
dockerManager: DockerManagerInstance; // +++ Add Docker Manager Instance +++
// currentSftpPath: Ref<string>; // 移除,由每个 sftpManager 内部管理
// --- 新增:独立编辑器状态 ---
editorTabs: Ref<FileTab[]>; // 编辑器标签页列表
@@ -152,6 +155,14 @@ export const useSessionStore = defineStore('session', () => {
isConnected: wsManager.isConnected,
};
const statusMonitorManager = createStatusMonitorManager(newSessionId, statusMonitorDeps);
// +++ Create Docker Manager Dependencies +++
const dockerManagerDeps: DockerManagerDependencies = {
sendMessage: wsManager.sendMessage,
onMessage: wsManager.onMessage,
isConnected: wsManager.isConnected,
};
// +++ Create Docker Manager Instance +++
const dockerManager = createDockerManager(newSessionId, dockerManagerDeps, { t });
// 2. 创建 SessionState 对象
const newSession: SessionState = {
@@ -163,6 +174,7 @@ export const useSessionStore = defineStore('session', () => {
sftpManagers: new Map<string, SftpManagerInstance>(), // 初始化 Map
terminalManager: terminalManager,
statusMonitorManager: statusMonitorManager,
dockerManager: dockerManager, // +++ Add Docker Manager to Session State +++
// currentSftpPath: currentSftpPath, // 移除
// --- 初始化编辑器状态 ---
editorTabs: ref([]), // 初始化为空数组
@@ -316,6 +328,8 @@ export const useSessionStore = defineStore('session', () => {
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 terminalManager.cleanup()`);
sessionToClose.statusMonitorManager.cleanup();
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 statusMonitorManager.cleanup()`);
sessionToClose.dockerManager.cleanup(); // +++ Call Docker Manager cleanup +++
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 dockerManager.cleanup()`); // +++ Add log +++
// TODO: 清理编辑器相关资源?例如提示保存未保存的文件
// 2. 从 Map 中移除会话 (需要创建 Map 的新实例以触发 shallowRef 更新)