Files
nexus-terminal/packages/frontend/src/composables/useDockerManager.ts
T
Baobhan Sith f7fe1904cf update
2025-04-23 20:35:50 +08:00

320 lines
14 KiB
TypeScript

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>;