Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion web/src/components/Docker/ContainerOverviewCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
33 changes: 31 additions & 2 deletions web/src/components/Docker/DockerConsoleViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { useDockerConsoleSessions } from '@/composables/useDockerConsoleSessions
interface Props {
containerName: string;
shell?: string;
isRunning?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
shell: 'sh',
isRunning: true,
});

const { getSession, createSession, showSession, hideSession, destroySession, markPoppedOut } =
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -93,6 +104,17 @@ watch(
}
);

watch(
() => props.isRunning,
(running) => {
if (running) {
initTerminal();
} else {
hideSession(props.containerName);
Comment on lines +110 to +113

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Destroy stale console session on stop

When a container stops, this watcher only hides the existing session. If the user then starts the container again, initTerminal() will see the cached session and skip createSession, so the iframe keeps pointing at a terminated ttyd connection and the console won’t reconnect until the user manually refreshes. This shows up when a container is stopped and then started while its console tab is open. Consider destroying the session (or forcing a new one) when isRunning becomes false, or bypassing the existing-session fast path on restart.

Useful? React with 👍 / 👎.

}
}
);

watch(showPlaceholder, (show) => {
if (show) {
requestAnimationFrame(updatePosition);
Expand Down Expand Up @@ -152,7 +174,14 @@ onBeforeUnmount(() => {
</div>
</div>

<div v-if="isConnecting" class="flex flex-1 items-center justify-center rounded-lg bg-black">
<div v-if="!isRunning" class="flex flex-1 items-center justify-center rounded-lg bg-black">
<div class="text-center">
<UIcon name="i-lucide-terminal" class="h-8 w-8 text-neutral-500" />
<p class="mt-2 text-sm text-neutral-400">Container is not running</p>
</div>
</div>

<div v-else-if="isConnecting" class="flex flex-1 items-center justify-center rounded-lg bg-black">
<div class="text-center">
<UIcon name="i-lucide-loader-2" class="h-8 w-8 animate-spin text-green-400" />
<p class="mt-2 text-sm text-green-400">Connecting to container...</p>
Expand Down
28 changes: 13 additions & 15 deletions web/src/components/Docker/DockerContainerManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -381,16 +381,23 @@ 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 },
{ label: 'Logs', value: 'logs' as const },
{
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,
},
]);
Comment on lines +384 to 402
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the actual file to see the full context
head -n 420 web/src/components/Docker/DockerContainerManagement.vue | tail -n 50

Repository: unraid/api

Length of output: 1523


🏁 Script executed:

# Search for UTabs usage with badge in the codebase
rg -n 'UTabs' web/src --type=vue -A 10 -B 2 | head -100

Repository: unraid/api

Length of output: 81


🏁 Script executed:

# Search for badge property usage patterns
rg -n 'badge\s*:' web/src --type=vue -A 2 -B 1

Repository: unraid/api

Length of output: 81


🏁 Script executed:

# Check if there's a package.json to see the @nuxt/ui version
cat web/package.json | grep -A 5 -B 5 '@nuxt/ui'

Repository: unraid/api

Length of output: 451


🏁 Script executed:

# Search for UTabs in files without specifying vue type (use general file search)
rg -n 'UTabs' web/src -A 10 -B 2 | head -150

Repository: unraid/api

Length of output: 2357


🏁 Script executed:

# Search for badge property patterns more broadly
rg -n 'badge' web/src --type=ts -A 2 -B 1 | head -100

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check the component template to see how UTabs is used with legacyPaneTabs
sed -n '415,500p' web/src/components/Docker/DockerContainerManagement.vue

Repository: unraid/api

Length of output: 2945


🌐 Web query:

@nuxt/ui 4.0.0-alpha.0 UTabs badge API documentation

💡 Result:

UTabs (v4.0.0‑alpha.0) — badge API (summary)

  • Each item in the UTabs items array may include a badge: badge?: string | number | BadgeProps. [1]. (ui4.nuxt.com)
  • You can pass a primitive (string/number) or a full BadgeProps object (so you can control label, color, variant, size, etc.). Use UI item.ui to customize classes (e.g. trailingBadge, trailingBadgeSize). [1][2]. (ui4.nuxt.com)

Example:

  • items: [{ label: 'Inbox', badge: 4 }, { label: 'Alerts', badge: { label: 'New', variant: 'subtle', size: 'sm' } }]. [1][2]. (ui4.nuxt.com)

Sources:

  • Tabs component (items + badge API). [1]. (ui4.nuxt.com)
  • Badge component (BadgeProps: label, color, variant, size, etc.). [2]. (ui4.nuxt.com)

Citations:


Badge object violates @nuxt/ui BadgeProps API—green dot indicator won't render properly.

The consoleBadge returns { color, variant, class }, but UTabs expects badge?: string | number | BadgeProps where BadgeProps only supports: label, color, variant, size. The class property is not a valid BadgeProps field and will be ignored. Additionally, there's no label, which may cause the badge to not display at all.

Fix: Either provide a label (e.g. label: '●' or label: '') and remove class, or apply Tailwind classes via the tab item's ui prop instead:

{
  label: 'Console',
  value: 'console' as const,
  badge: isContainerRunning.value && hasActiveConsoleSession.value 
    ? { label: '●', color: 'success', size: 'xs' }
    : undefined,
  ui: { label: isContainerRunning.value && hasActiveConsoleSession.value ? 'text-success-500' : '' }
}


Expand Down Expand Up @@ -550,12 +557,6 @@ const [transitionContainerRef] = useAutoAnimate({
{{ stripLeadingSlash(activeContainer?.names?.[0]) || 'Container' }}
</div>
</div>
<UBadge
v-if="activeContainer?.state"
:label="activeContainer.state"
color="primary"
variant="subtle"
/>
</div>
<UTabs
v-model="legacyPaneTab"
Expand Down Expand Up @@ -606,6 +607,7 @@ const [transitionContainerRef] = useAutoAnimate({
:container-name="stripLeadingSlash(activeContainer.names?.[0])"
:auto-scroll="logAutoScroll"
:client-filter="logFilterText"
:is-running="isContainerRunning"
class="h-full flex-1"
/>
</div>
Expand All @@ -617,6 +619,7 @@ const [transitionContainerRef] = useAutoAnimate({
v-if="activeContainer"
:container-name="activeContainerName"
:shell="activeContainer.shell ?? 'sh'"
:is-running="isContainerRunning"
class="h-full"
/>
</div>
Expand All @@ -636,12 +639,6 @@ const [transitionContainerRef] = useAutoAnimate({
/>
<div class="font-medium">Overview</div>
</div>
<UBadge
v-if="activeContainer?.state"
:label="activeContainer.state"
color="primary"
variant="subtle"
/>
</div>
</template>
<div class="relative">
Expand Down Expand Up @@ -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"
/>
</div>
Expand Down
4 changes: 4 additions & 0 deletions web/src/components/Docker/SingleDockerLogViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ const props = withDefaults(
containerName: string;
autoScroll: boolean;
clientFilter?: string;
isRunning?: boolean;
}>(),
{
clientFilter: '',
isRunning: true,
}
);

Expand Down Expand Up @@ -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"
/>
</template>
8 changes: 7 additions & 1 deletion web/src/components/Logs/BaseLogViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface Props {
loadingMoreContent?: boolean;
isAtTop?: boolean;
canLoadMore?: boolean;
dimmed?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
Expand All @@ -42,6 +43,7 @@ const props = withDefaults(defineProps<Props>(), {
loadingMoreContent: false,
isAtTop: false,
canLoadMore: false,
dimmed: false,
});

const emit = defineEmits<{
Expand Down Expand Up @@ -168,7 +170,11 @@ defineExpose({ forceScrollToBottom, scrollViewportRef });

<pre
class="hljs m-0 p-4 font-mono text-xs leading-6 whitespace-pre"
:class="{ 'theme-dark': isDarkMode, 'theme-light': !isDarkMode }"
:class="{
'theme-dark': isDarkMode,
'theme-light': !isDarkMode,
'opacity-50': dimmed,
}"
v-html="highlightedContent"
/>
</div>
Expand Down
5 changes: 4 additions & 1 deletion web/src/composables/useDockerRowActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand All @@ -229,6 +231,7 @@ export function useDockerRowActions(options: DockerRowActionsOptions) {
icon: 'i-lucide-terminal',
as: 'button',
color: hasConsoleSession ? 'success' : undefined,
disabled: !isRunning,
onSelect: () => onOpenConsole(row),
},
{
Expand Down
14 changes: 10 additions & 4 deletions web/src/composables/useDockerTableColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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<string, 'success' | 'warning' | 'neutral'> = {
const colorMap: Record<string, 'success' | 'warning' | 'error' | 'neutral'> = {
[ContainerState.RUNNING]: 'success',
[ContainerState.PAUSED]: 'warning',
[ContainerState.EXITED]: 'neutral',
[ContainerState.EXITED]: 'error',
};
const labelMap: Record<string, string> = {
[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' });
},
},
{
Expand Down
Loading