Skip to content

Commit 678f8f2

Browse files
mkaltnerMichael Kaltner
authored andcommitted
refactor(containers): move compose detection to backend and add i18n
- Add ComposeInfo to backend container Details response with Docker Compose metadata - Extract compose project/service info from labels in Go (backend-side processing) - Add i18n strings for all hardcoded text in ContainerComposePanel component - Simplify frontend logic to use composeInfo from API instead of label parsing - Fix Svelte reactivity pattern to properly track source content changes - Remove redundant Svelte effect antipattern per code review feedback Addresses code review feedback to move heavy lifting to backend and ensure proper internationalization coverage.
1 parent 788ba6c commit 678f8f2

File tree

6 files changed

+102
-19
lines changed

6 files changed

+102
-19
lines changed

frontend/messages/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,12 @@
513513
"container_hostname_placeholder": "my-hostname",
514514
"container_domain_label": "Domain Name",
515515
"container_domain_placeholder": "example.com",
516+
"container_compose_gitops_managed_title": "GitOps Managed — Read Only",
517+
"container_compose_gitops_managed_description": "This project is managed by GitOps ({provider}). The compose file is read-only and can only be changed via your Git repository.",
518+
"container_compose_editing_info": "Editing {file} for project {project}. This container runs as the {service} service.",
519+
"container_compose_save_success": "Compose file saved successfully",
520+
"container_compose_save_failed": "Failed to save compose file",
521+
"container_compose_view_project": "View Project",
516522
"tabs_environment": "Environment",
517523
"tabs_volumes": "Volumes",
518524
"tabs_network_security": "Network & Security",

frontend/src/lib/types/container.type.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ export interface ContainerConfigDto {
160160
user?: string;
161161
}
162162

163+
export interface ComposeInfo {
164+
projectName: string;
165+
serviceName: string;
166+
configFile?: string;
167+
workingDir?: string;
168+
projectDir?: string;
169+
}
170+
163171
export interface ContainerDetailsDto {
164172
id: string;
165173
name: string;
@@ -173,6 +181,7 @@ export interface ContainerDetailsDto {
173181
ports: ContainerPorts[];
174182
mounts: ContainerMounts[];
175183
labels: Record<string, string>;
184+
composeInfo?: ComposeInfo;
176185
}
177186

178187
// Container Stats Types

frontend/src/routes/(app)/containers/[containerId]/+page.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,15 @@
227227
const showShell = $derived(!!container?.state?.running);
228228
229229
const project = $derived(data?.project ?? null);
230-
const composeServiceName = $derived(container?.labels?.['com.docker.compose.service'] ?? '');
230+
const composeInfo = $derived(container?.composeInfo ?? null);
231+
const composeServiceName = $derived(composeInfo?.serviceName ?? '');
231232
232233
// Find which file (root compose or an include file) directly defines this service.
233234
// Returns { includeFile: null } for root compose, { includeFile: <file> } for a sub-file,
234235
// or null if the service isn't found anywhere (hides the tab).
235236
const serviceComposeSource = $derived(
236237
(() => {
237-
if (!project || !composeServiceName) return null;
238+
if (!project || !composeServiceName || !composeInfo) return null;
238239
239240
const hasService = (content: string): boolean => {
240241
try {
@@ -259,7 +260,7 @@
259260
})()
260261
);
261262
262-
const showComposeTab = $derived(!!serviceComposeSource);
263+
const showComposeTab = $derived(!!composeInfo && !!serviceComposeSource);
263264
264265
const tabItems = $derived<TabItem[]>([
265266
{ value: 'overview', label: m.common_overview(), icon: ContainersIcon },

frontend/src/routes/(app)/containers/[containerId]/+page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const load: PageLoad = async ({ params, parent }) => {
2828
}
2929

3030
let project = null;
31-
const composeProjectName = container.labels?.['com.docker.compose.project'];
31+
const composeProjectName = container.composeInfo?.projectName;
3232
if (composeProjectName) {
3333
try {
3434
const projectsResult = await projectService.getProjectsForEnvironment(envId, { search: composeProjectName });

frontend/src/routes/(app)/containers/components/ContainerComposePanel.svelte

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { toast } from 'svelte-sonner';
77
import type { Project, IncludeFile } from '$lib/types/project.type';
88
import { AlertIcon, ExternalLinkIcon } from '$lib/icons';
9-
import { untrack } from 'svelte';
9+
import * as m from '$lib/paraglide/messages';
1010
1111
let {
1212
project,
@@ -20,9 +20,15 @@
2020
2121
const sourceContent = $derived(includeFile ? includeFile.content : (project.composeContent ?? ''));
2222
23-
let composeContent = $state(untrack(() => sourceContent));
23+
let composeContent = $state(sourceContent);
24+
25+
// Update composeContent when source changes (e.g., switching containers)
26+
let prevSourceContent = $state(sourceContent);
2427
$effect(() => {
25-
composeContent = sourceContent;
28+
if (sourceContent !== prevSourceContent) {
29+
composeContent = sourceContent;
30+
prevSourceContent = sourceContent;
31+
}
2632
});
2733
2834
let panelOpen = $state(true);
@@ -41,9 +47,9 @@
4147
} else {
4248
await projectService.updateProject(project.id, undefined, composeContent);
4349
}
44-
toast.success('Compose file saved successfully');
50+
toast.success(m.container_compose_save_success());
4551
} catch (err: any) {
46-
saveError = err?.message ?? 'Failed to save compose file';
52+
saveError = err?.message ?? m.container_compose_save_failed();
4753
toast.error(saveError as string);
4854
} finally {
4955
isSaving = false;
@@ -55,19 +61,20 @@
5561
{#if project.gitOpsManagedBy}
5662
<Alert.Root variant="default">
5763
<AlertIcon class="size-4" />
58-
<Alert.Title>GitOps Managed — Read Only</Alert.Title>
64+
<Alert.Title>{m.container_compose_gitops_managed_title()}</Alert.Title>
5965
<Alert.Description>
60-
This project is managed by GitOps (<strong>{project.gitOpsManagedBy}</strong>). The compose file is read-only and can only
61-
be changed via your Git repository.
66+
{@html m.container_compose_gitops_managed_description({ provider: `<strong>${project.gitOpsManagedBy}</strong>` })}
6267
</Alert.Description>
6368
</Alert.Root>
6469
{/if}
6570

6671
<div class="bg-muted flex items-start gap-2 rounded-lg border px-4 py-3 text-sm">
6772
<span>
68-
Editing <strong>{fileTitle}</strong> for project
69-
<a href="/projects/{project.id}" class="text-primary font-medium hover:underline">{project.name}</a>. This container runs as
70-
the <strong>{serviceName}</strong> service.
73+
{@html m.container_compose_editing_info({
74+
file: `<strong>${fileTitle}</strong>`,
75+
project: `<a href="/projects/${project.id}" class="text-primary font-medium hover:underline">${project.name}</a>`,
76+
service: `<strong>${serviceName}</strong>`
77+
})}
7178
</span>
7279
</div>
7380

@@ -86,6 +93,11 @@
8693
{#if !isReadOnly}
8794
<ArcaneButton action="save" loading={isSaving} onclick={handleSave} />
8895
{/if}
89-
<ArcaneButton action="base" href="/projects/{project.id}" icon={ExternalLinkIcon} customLabel="View Project" />
96+
<ArcaneButton
97+
action="base"
98+
href="/projects/{project.id}"
99+
icon={ExternalLinkIcon}
100+
customLabel={m.container_compose_view_project()}
101+
/>
90102
</div>
91103
</div>

types/container/container.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,34 @@ type Summary struct {
634634
UpdateInfo *imagetypes.UpdateInfo `json:"updateInfo,omitempty"`
635635
}
636636

637+
// ComposeInfo contains Docker Compose project information extracted from container labels.
638+
type ComposeInfo struct {
639+
// ProjectName is the name of the Docker Compose project.
640+
//
641+
// Required: true
642+
ProjectName string `json:"projectName"`
643+
644+
// ServiceName is the name of the service within the Compose project.
645+
//
646+
// Required: true
647+
ServiceName string `json:"serviceName"`
648+
649+
// ConfigFile is the path to the compose config file.
650+
//
651+
// Required: false
652+
ConfigFile string `json:"configFile,omitempty"`
653+
654+
// WorkingDir is the working directory of the Compose project.
655+
//
656+
// Required: false
657+
WorkingDir string `json:"workingDir,omitempty"`
658+
659+
// ProjectDir is the project directory of the Compose project.
660+
//
661+
// Required: false
662+
ProjectDir string `json:"projectDir,omitempty"`
663+
}
664+
637665
// Details represents detailed container information.
638666
type Details struct {
639667
// ID is the unique identifier of the container.
@@ -695,6 +723,12 @@ type Details struct {
695723
//
696724
// Required: false
697725
Labels map[string]string `json:"labels,omitempty"`
726+
727+
// ComposeInfo contains Docker Compose project information.
728+
// Only present if container is part of a Compose project.
729+
//
730+
// Required: false
731+
ComposeInfo *ComposeInfo `json:"composeInfo,omitempty"`
698732
}
699733

700734
// Created represents a newly created container.
@@ -874,6 +908,26 @@ func NewDetails(c *container.InspectResponse) Details {
874908
}
875909
}
876910

911+
// Extract Docker Compose information from labels if present
912+
var composeInfo *ComposeInfo
913+
if projectName, hasProject := labels["com.docker.compose.project"]; hasProject {
914+
if serviceName, hasService := labels["com.docker.compose.service"]; hasService {
915+
composeInfo = &ComposeInfo{
916+
ProjectName: projectName,
917+
ServiceName: serviceName,
918+
}
919+
if configFile, ok := labels["com.docker.compose.config-files"]; ok {
920+
composeInfo.ConfigFile = configFile
921+
}
922+
if workingDir, ok := labels["com.docker.compose.project.working_dir"]; ok {
923+
composeInfo.WorkingDir = workingDir
924+
}
925+
if projectDir, ok := labels["com.docker.compose.project.config_files"]; ok {
926+
composeInfo.ProjectDir = projectDir
927+
}
928+
}
929+
}
930+
877931
return Details{
878932
ID: c.ID,
879933
Name: name,
@@ -886,9 +940,10 @@ func NewDetails(c *container.InspectResponse) Details {
886940
NetworkSettings: NetworkSettings{
887941
Networks: networks,
888942
},
889-
Ports: ports,
890-
Mounts: mounts,
891-
Labels: labels,
943+
Ports: ports,
944+
Mounts: mounts,
945+
Labels: labels,
946+
ComposeInfo: composeInfo,
892947
}
893948
}
894949

0 commit comments

Comments
 (0)