From 2c2f19ae3ee50f6169d3a9f317f5f166c44410cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:38:58 +0000 Subject: [PATCH 1/4] Initial plan From 3e034a1218c718544a8f2011e9cd5f22f9209f5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:45:30 +0000 Subject: [PATCH 2/4] Add automatic grouping for Docker Swarm stack containers --- CHANGELOG.md | 4 ++ .../containers/ContainerProperties.test.ts | 40 +++++++++++++++++++ src/tree/containers/ContainerGroupTreeItem.ts | 11 ++++- src/tree/containers/ContainerProperties.ts | 19 ++++++++- 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/test/tree/containers/ContainerProperties.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 09bd19f9..0a0fb099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased +### Added +* Docker Swarm stack containers are now automatically grouped by their stack name in the Containers view (when grouping by Compose Project Name), matching the experience for Docker Compose projects. [#472](https://github.com/microsoft/vscode-containers/issues/472) + ## 2.4.5 - 28 May 2026 ### Changed * Deployments to Azure App Service are now performed through the [Azure App Service extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureappservice), simplifying the deployment flow and reducing duplicated code. [#473](https://github.com/microsoft/vscode-containers/pull/473) diff --git a/src/test/tree/containers/ContainerProperties.test.ts b/src/test/tree/containers/ContainerProperties.test.ts new file mode 100644 index 00000000..73120b9e --- /dev/null +++ b/src/test/tree/containers/ContainerProperties.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ListContainersItem } from '@microsoft/vscode-container-client'; +import assert from 'assert'; +import { getComposeProjectGroup, NonComposeGroupName } from '../../../tree/containers/ContainerProperties'; + +function makeContainer(labels: { [key: string]: string } | undefined): ListContainersItem { + return { + labels: labels, + } as ListContainersItem; +} + +suite('(unit) ContainerProperties.getComposeProjectGroup', () => { + test('Groups compose containers by their compose project name', () => { + const container = makeContainer({ 'com.docker.compose.project': 'myproject' }); + assert.strictEqual(getComposeProjectGroup(container), 'myproject'); + }); + + test('Groups swarm stack containers by their stack namespace', () => { + const container = makeContainer({ 'com.docker.stack.namespace': 'mystack' }); + assert.strictEqual(getComposeProjectGroup(container), 'mystack'); + }); + + test('Prefers the compose project name when both labels are present', () => { + const container = makeContainer({ + 'com.docker.compose.project': 'myproject', + 'com.docker.stack.namespace': 'mystack', + }); + assert.strictEqual(getComposeProjectGroup(container), 'myproject'); + }); + + test('Returns the non-compose group name for standalone containers', () => { + assert.strictEqual(getComposeProjectGroup(makeContainer({})), NonComposeGroupName); + assert.strictEqual(getComposeProjectGroup(makeContainer(undefined)), NonComposeGroupName); + assert.strictEqual(getComposeProjectGroup(makeContainer({ 'some.other.label': 'value' })), NonComposeGroupName); + }); +}); diff --git a/src/tree/containers/ContainerGroupTreeItem.ts b/src/tree/containers/ContainerGroupTreeItem.ts index 600036be..8da7d66b 100644 --- a/src/tree/containers/ContainerGroupTreeItem.ts +++ b/src/tree/containers/ContainerGroupTreeItem.ts @@ -8,7 +8,8 @@ import { ThemeIcon, TreeItemCollapsibleState } from "vscode"; import { LocalGroupTreeItemBase } from "../LocalGroupTreeItemBase"; import { LocalRootTreeItemBase } from "../LocalRootTreeItemBase"; import { getCommonGroupIcon } from "../settings/CommonProperties"; -import { ContainerProperty, getContainerStateIcon, NonComposeGroupName } from "./ContainerProperties"; +import { ContainerProperty, composeProjectLabel, getContainerStateIcon, NonComposeGroupName } from "./ContainerProperties"; +import { ContainerTreeItem } from "./ContainerTreeItem"; import { DockerContainerInfo } from "./ContainersTreeItem"; export class ContainerGroupTreeItem extends LocalGroupTreeItemBase { @@ -26,13 +27,19 @@ export class ContainerGroupTreeItem extends LocalGroupTreeItemBase !!c.labels?.[composeProjectLabel]); + } + public get iconPath(): ThemeIcon { switch (this.parent.groupBySetting) { case 'ContainerId': diff --git a/src/tree/containers/ContainerProperties.ts b/src/tree/containers/ContainerProperties.ts index 2abbe2fe..6433b69b 100644 --- a/src/tree/containers/ContainerProperties.ts +++ b/src/tree/containers/ContainerProperties.ts @@ -20,7 +20,7 @@ export const containerProperties: ITreePropertyInfo[] = [ { property: 'Ports', exampleValue: '8080' }, { property: 'State', exampleValue: 'exited' }, { property: 'Status', exampleValue: 'Exited (0) 2 hours ago' }, - { property: 'Compose Project Name', description: l10n.t('Value used to associate containers launched by a compose up command') }, + { property: 'Compose Project Name', description: l10n.t('Value used to associate containers launched by a compose up command or a Docker Swarm stack') }, { property: 'Label', exampleValue: 'com.microsoft.created-by=visual-studio-code' }, ]; @@ -64,7 +64,7 @@ export function getContainerPropertyValue(item: ListContainersItem, property: Co // This normalizes things like "10 seconds" and "Less than a second" to "Less than a minute", meaning the refreshes don't happen constantly return item.status?.replace(/(\d+ seconds?)|(Less than a second)/i, l10n.t('Less than a minute')); case 'Compose Project Name': - return getLabelGroup(item, 'com.docker.compose.project', NonComposeGroupName); + return getComposeProjectGroup(item); case 'Image': return item.image.originalName; case 'Label': @@ -77,6 +77,21 @@ export function getContainerPropertyValue(item: ListContainersItem, property: Co export const NonComposeGroupName = l10n.t('Individual Containers'); export const NonLabelGroupName = l10n.t('Others'); +// The label Docker Compose adds to containers it creates, containing the compose project name +export const composeProjectLabel = 'com.docker.compose.project'; + +// The label Docker Swarm adds to containers it creates as part of a stack, containing the stack namespace (name) +export const swarmStackNamespaceLabel = 'com.docker.stack.namespace'; + +export function getComposeProjectGroup(container: ListContainersItem): string { + // Containers launched by `docker compose up` are grouped by their compose project name. + // Containers launched by `docker stack deploy` (Docker Swarm) are grouped by their stack namespace. + // Either way, this gives a meaningful group name for orchestrated containers, falling back to the + // non-compose group name for standalone containers. + return getLabelGroup(container, composeProjectLabel, '') + || getLabelGroup(container, swarmStackNamespaceLabel, NonComposeGroupName); +} + export function getLabelGroup(container: ListContainersItem, label: string | undefined, defaultGroupName: string): string { if (!label) { return defaultGroupName; From a8d7d5cb45270d5f7d85f441a85e8a200043047f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:36:21 +0000 Subject: [PATCH 3/4] Merge main and relocate swarm test to monorepo path --- .../src}/test/tree/containers/ContainerProperties.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src => extensions/vscode-containers/src}/test/tree/containers/ContainerProperties.test.ts (100%) diff --git a/src/test/tree/containers/ContainerProperties.test.ts b/extensions/vscode-containers/src/test/tree/containers/ContainerProperties.test.ts similarity index 100% rename from src/test/tree/containers/ContainerProperties.test.ts rename to extensions/vscode-containers/src/test/tree/containers/ContainerProperties.test.ts From 8a4f9e22411fff520999809655e4f7778ed05b73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:05:44 +0000 Subject: [PATCH 4/4] Fix PR review comments: revert CHANGELOG, fix import, fix isComposeProjectGroup eagerness --- extensions/vscode-containers/CHANGELOG.md | 4 ---- .../vscode-containers/src/tree/LocalGroupTreeItemBase.ts | 4 ++++ .../src/tree/containers/ContainerGroupTreeItem.ts | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/extensions/vscode-containers/CHANGELOG.md b/extensions/vscode-containers/CHANGELOG.md index 0a0fb099..09bd19f9 100644 --- a/extensions/vscode-containers/CHANGELOG.md +++ b/extensions/vscode-containers/CHANGELOG.md @@ -1,7 +1,3 @@ -## Unreleased -### Added -* Docker Swarm stack containers are now automatically grouped by their stack name in the Containers view (when grouping by Compose Project Name), matching the experience for Docker Compose projects. [#472](https://github.com/microsoft/vscode-containers/issues/472) - ## 2.4.5 - 28 May 2026 ### Changed * Deployments to Azure App Service are now performed through the [Azure App Service extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azureappservice), simplifying the deployment flow and reducing duplicated code. [#473](https://github.com/microsoft/vscode-containers/pull/473) diff --git a/extensions/vscode-containers/src/tree/LocalGroupTreeItemBase.ts b/extensions/vscode-containers/src/tree/LocalGroupTreeItemBase.ts index 61d69cf7..8a36f0f0 100644 --- a/extensions/vscode-containers/src/tree/LocalGroupTreeItemBase.ts +++ b/extensions/vscode-containers/src/tree/LocalGroupTreeItemBase.ts @@ -41,6 +41,10 @@ export abstract class LocalGroupTreeItemBase { @@ -37,7 +36,7 @@ export class ContainerGroupTreeItem extends LocalGroupTreeItemBase !!c.labels?.[composeProjectLabel]); + return this.items.some(item => !!item.labels?.[composeProjectLabel]); } public get iconPath(): ThemeIcon {