diff --git a/web/src/components/Docker/ContainerOverviewCard.vue b/web/src/components/Docker/ContainerOverviewCard.vue index 0b4578627a..cd958c2c40 100644 --- a/web/src/components/Docker/ContainerOverviewCard.vue +++ b/web/src/components/Docker/ContainerOverviewCard.vue @@ -40,7 +40,8 @@ const stateColor = computed(() => { const stateLabel = computed(() => { const state = props.container?.state; if (!state) return 'Unknown'; - return state.charAt(0).toUpperCase() + state.slice(1); + if (state === 'EXITED') return 'Stopped'; + return state.charAt(0).toUpperCase() + state.slice(1).toLowerCase(); }); const imageVersion = computed(() => formatImage(props.container)); diff --git a/web/src/components/Docker/DockerConsoleViewer.vue b/web/src/components/Docker/DockerConsoleViewer.vue index ec215f3933..66b9d82927 100644 --- a/web/src/components/Docker/DockerConsoleViewer.vue +++ b/web/src/components/Docker/DockerConsoleViewer.vue @@ -6,10 +6,12 @@ import { useDockerConsoleSessions } from '@/composables/useDockerConsoleSessions interface Props { containerName: string; shell?: string; + isRunning?: boolean; } const props = withDefaults(defineProps(), { shell: 'sh', + isRunning: true, }); const { getSession, createSession, showSession, hideSession, destroySession, markPoppedOut } = @@ -26,7 +28,9 @@ const socketPath = computed(() => { return `/logterminal/${encodedName}/`; }); -const showPlaceholder = computed(() => !isConnecting.value && !hasError.value && !isPoppedOut.value); +const showPlaceholder = computed( + () => props.isRunning && !isConnecting.value && !hasError.value && !isPoppedOut.value +); function updatePosition() { if (placeholderRef.value && showPlaceholder.value) { @@ -36,6 +40,13 @@ function updatePosition() { } async function initTerminal() { + if (!props.isRunning) { + isConnecting.value = false; + hasError.value = false; + isPoppedOut.value = false; + return; + } + const existingSession = getSession(props.containerName); if (existingSession && !existingSession.isPoppedOut) { @@ -93,6 +104,17 @@ watch( } ); +watch( + () => props.isRunning, + (running) => { + if (running) { + initTerminal(); + } else { + hideSession(props.containerName); + } + } +); + watch(showPlaceholder, (show) => { if (show) { requestAnimationFrame(updatePosition); @@ -152,7 +174,14 @@ onBeforeUnmount(() => { -
+
+
+ +

Container is not running

+
+
+ +

Connecting to container...

diff --git a/web/src/components/Docker/DockerContainerManagement.vue b/web/src/components/Docker/DockerContainerManagement.vue index 9e277e58fb..4cd0cc804c 100644 --- a/web/src/components/Docker/DockerContainerManagement.vue +++ b/web/src/components/Docker/DockerContainerManagement.vue @@ -381,6 +381,15 @@ const hasActiveConsoleSession = computed(() => { return name ? hasActiveSession(name) : false; }); +const isContainerRunning = computed(() => activeContainer.value?.state === 'RUNNING'); + +const consoleBadge = computed(() => { + if (isContainerRunning.value && hasActiveConsoleSession.value) { + return { color: 'success' as const, variant: 'solid' as const, class: 'w-2 h-2 p-0 min-w-0' }; + } + return undefined; +}); + const legacyPaneTabs = computed(() => [ { label: 'Overview', value: 'overview' as const }, { label: 'Settings', value: 'settings' as const }, @@ -388,9 +397,7 @@ const legacyPaneTabs = computed(() => [ { label: 'Console', value: 'console' as const, - badge: hasActiveConsoleSession.value - ? { color: 'success' as const, variant: 'solid' as const, class: 'w-2 h-2 p-0 min-w-0' } - : undefined, + badge: consoleBadge.value, }, ]); @@ -550,12 +557,6 @@ const [transitionContainerRef] = useAutoAnimate({ {{ stripLeadingSlash(activeContainer?.names?.[0]) || 'Container' }}
-
@@ -617,6 +619,7 @@ const [transitionContainerRef] = useAutoAnimate({ v-if="activeContainer" :container-name="activeContainerName" :shell="activeContainer.shell ?? 'sh'" + :is-running="isContainerRunning" class="h-full" /> @@ -636,12 +639,6 @@ const [transitionContainerRef] = useAutoAnimate({ />
Overview
-
@@ -684,6 +681,7 @@ const [transitionContainerRef] = useAutoAnimate({ v-if="activeContainer" :container-name="stripLeadingSlash(activeContainer.names?.[0])" :auto-scroll="true" + :is-running="isContainerRunning" class="h-full" />
diff --git a/web/src/components/Docker/SingleDockerLogViewer.vue b/web/src/components/Docker/SingleDockerLogViewer.vue index becf9ae302..1e97abddde 100644 --- a/web/src/components/Docker/SingleDockerLogViewer.vue +++ b/web/src/components/Docker/SingleDockerLogViewer.vue @@ -16,9 +16,11 @@ const props = withDefaults( containerName: string; autoScroll: boolean; clientFilter?: string; + isRunning?: boolean; }>(), { clientFilter: '', + isRunning: true, } ); @@ -201,6 +203,8 @@ defineExpose({ refreshLogContent }); :auto-scroll="autoScroll" :show-refresh="true" :show-download="false" + :dimmed="!isRunning" + :additional-info="!isRunning ? 'Container stopped - showing historical logs' : ''" @refresh="refreshLogContent" /> diff --git a/web/src/components/Logs/BaseLogViewer.vue b/web/src/components/Logs/BaseLogViewer.vue index 99ca951c55..8ee65cc0ce 100644 --- a/web/src/components/Logs/BaseLogViewer.vue +++ b/web/src/components/Logs/BaseLogViewer.vue @@ -27,6 +27,7 @@ interface Props { loadingMoreContent?: boolean; isAtTop?: boolean; canLoadMore?: boolean; + dimmed?: boolean; } const props = withDefaults(defineProps(), { @@ -42,6 +43,7 @@ const props = withDefaults(defineProps(), { loadingMoreContent: false, isAtTop: false, canLoadMore: false, + dimmed: false, }); const emit = defineEmits<{ @@ -168,7 +170,11 @@ defineExpose({ forceScrollToBottom, scrollViewportRef });
     
diff --git a/web/src/composables/useDockerRowActions.ts b/web/src/composables/useDockerRowActions.ts
index 7c99fa6e65..58a44e789b 100644
--- a/web/src/composables/useDockerRowActions.ts
+++ b/web/src/composables/useDockerRowActions.ts
@@ -215,7 +215,9 @@ export function useDockerRowActions(options: DockerRowActionsOptions) {
     ]);
 
     const containerName = getContainerNameFromRow(row);
-    const hasConsoleSession = containerName ? hasActiveConsoleSession(containerName) : false;
+    const isRunning = row.meta?.state === ContainerState.RUNNING;
+    const hasConsoleSession =
+      containerName && isRunning ? hasActiveConsoleSession(containerName) : false;
 
     items.push([
       {
@@ -229,6 +231,7 @@ export function useDockerRowActions(options: DockerRowActionsOptions) {
         icon: 'i-lucide-terminal',
         as: 'button',
         color: hasConsoleSession ? 'success' : undefined,
+        disabled: !isRunning,
         onSelect: () => onOpenConsole(row),
       },
       {
diff --git a/web/src/composables/useDockerTableColumns.ts b/web/src/composables/useDockerTableColumns.ts
index 698f2d2f9c..88d2ad1807 100644
--- a/web/src/composables/useDockerTableColumns.ts
+++ b/web/src/composables/useDockerTableColumns.ts
@@ -54,7 +54,7 @@ export function useDockerTableColumns(options: DockerTableColumnsOptions) {
     options;
 
   const UButton = resolveComponent('UButton');
-  const UBadge = resolveComponent('UBadge');
+  const UBadge = resolveComponent('UBadge') as Component;
   const UDropdownMenu = resolveComponent('UDropdownMenu');
   const USkeleton = resolveComponent('USkeleton') as Component;
   const UIcon = resolveComponent('UIcon');
@@ -106,16 +106,22 @@ export function useDockerTableColumns(options: DockerTableColumnsOptions) {
           if (row.original.type === 'folder') return '';
           const state = row.original.state ?? '';
           const isBusy = busyRowIds.value.has(row.original.id);
-          const colorMap: Record = {
+          const colorMap: Record = {
             [ContainerState.RUNNING]: 'success',
             [ContainerState.PAUSED]: 'warning',
-            [ContainerState.EXITED]: 'neutral',
+            [ContainerState.EXITED]: 'error',
+          };
+          const labelMap: Record = {
+            [ContainerState.RUNNING]: 'Running',
+            [ContainerState.PAUSED]: 'Paused',
+            [ContainerState.EXITED]: 'Stopped',
           };
           const color = colorMap[state] || 'neutral';
+          const label = labelMap[state] || state;
           if (isBusy) {
             return h(USkeleton, { class: 'h-5 w-20' });
           }
-          return h(UBadge, { color }, () => state);
+          return h(UBadge, { color, label, variant: 'subtle' });
         },
       },
       {