update
This commit is contained in:
@@ -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 更新)
|
||||
|
||||
Reference in New Issue
Block a user