Skip to content

Commit f012428

Browse files
committed
feat(containers): show and edit the specific service's compose file (including sub-files)
When a container belongs to a project with `include:` directives, find the include file that directly defines the service and show/edit that instead of the root wrapper compose. - serviceComposeSource derived: scans root compose, then includeFiles[], to find which file has `services.{serviceName}` defined - Shows tab for both root-defined and include-file-defined services - Hides tab only when the service isn't found in any file - ContainerComposePanel now accepts optional `includeFile` prop - Displays includeFile.content instead of project.composeContent - Saves via projectService.updateProjectIncludeFile (relativePath + content) - Falls back to updateProject for root compose - fileTitle shows the actual file name (e.g. ollama/compose.yml) - fileId scoped per include file so editor state doesn't collide
1 parent 2a6274c commit f012428

File tree

2 files changed

+51
-23
lines changed

2 files changed

+51
-23
lines changed

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
CodeIcon
4242
} from '$lib/icons';
4343
import { parse as parseYaml } from 'yaml';
44+
import type { IncludeFile } from '$lib/types/project.type';
4445
let { data } = $props();
4546
let container = $derived(data?.container as ContainerDetailsDto);
4647
let stats = $state(null as ContainerStatsType | null);
@@ -228,25 +229,38 @@
228229
const project = $derived(data?.project ?? null);
229230
const composeServiceName = $derived(container?.labels?.['com.docker.compose.service'] ?? '');
230231
231-
// Only show the Compose tab if the service is directly defined in the project's compose file.
232-
// When a project uses `include:` directives, the root compose is just a wrapper — not useful
233-
// to edit from a container view. In that case we hide the tab entirely.
234-
const showComposeTab = $derived(
235-
!!project?.composeContent &&
236-
!!composeServiceName &&
237-
(() => {
232+
// Find which file (root compose or an include file) directly defines this service.
233+
// Returns { includeFile: null } for root compose, { includeFile: <file> } for a sub-file,
234+
// or null if the service isn't found anywhere (hides the tab).
235+
const serviceComposeSource = $derived(
236+
(() => {
237+
if (!project || !composeServiceName) return null;
238+
239+
const hasService = (content: string): boolean => {
238240
try {
239-
const parsed = parseYaml(project!.composeContent!) as Record<string, unknown> | null;
240-
return (
241-
!!parsed?.services &&
242-
!!(parsed.services as Record<string, unknown>)[composeServiceName]
243-
);
241+
const parsed = parseYaml(content) as Record<string, unknown> | null;
242+
return !!(parsed?.services && (parsed.services as Record<string, unknown>)[composeServiceName]);
244243
} catch {
245244
return false;
246245
}
247-
})()
246+
};
247+
248+
if (project.composeContent && hasService(project.composeContent)) {
249+
return { includeFile: null as IncludeFile | null };
250+
}
251+
252+
for (const f of project.includeFiles ?? []) {
253+
if (hasService(f.content)) {
254+
return { includeFile: f };
255+
}
256+
}
257+
258+
return null;
259+
})()
248260
);
249261
262+
const showComposeTab = $derived(!!serviceComposeSource);
263+
250264
const tabItems = $derived<TabItem[]>([
251265
{ value: 'overview', label: m.common_overview(), icon: ContainersIcon },
252266
...(showStats ? [{ value: 'stats', label: m.containers_nav_metrics(), icon: StatsIcon }] : []),
@@ -410,9 +424,13 @@
410424
</Tabs.Content>
411425
{/if}
412426

413-
{#if project && showComposeTab}
427+
{#if project && serviceComposeSource}
414428
<Tabs.Content value="compose" class="h-full min-h-0">
415-
<ContainerComposePanel {project} serviceName={composeServiceName} />
429+
<ContainerComposePanel
430+
{project}
431+
serviceName={composeServiceName}
432+
includeFile={serviceComposeSource.includeFile}
433+
/>
416434
</Tabs.Content>
417435
{/if}
418436
{/snippet}

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,43 @@
44
import CodePanel from '../../projects/components/CodePanel.svelte';
55
import { projectService } from '$lib/services/project-service';
66
import { toast } from 'svelte-sonner';
7-
import type { Project } from '$lib/types/project.type';
7+
import type { Project, IncludeFile } from '$lib/types/project.type';
88
import { AlertIcon, ExternalLinkIcon } from '$lib/icons';
99
import { untrack } from 'svelte';
1010
1111
let {
1212
project,
13-
serviceName
13+
serviceName,
14+
includeFile = null
1415
}: {
1516
project: Project;
1617
serviceName: string;
18+
includeFile?: IncludeFile | null;
1719
} = $props();
1820
19-
let composeContent = $state(untrack(() => project.composeContent ?? ''));
21+
const sourceContent = $derived(includeFile ? includeFile.content : (project.composeContent ?? ''));
22+
23+
let composeContent = $state(untrack(() => sourceContent));
2024
$effect(() => {
21-
composeContent = project.composeContent ?? '';
25+
composeContent = sourceContent;
2226
});
27+
2328
let panelOpen = $state(true);
2429
let isSaving = $state(false);
2530
let saveError = $state('');
2631
2732
const isReadOnly = $derived(!!project.gitOpsManagedBy);
33+
const fileTitle = $derived(includeFile ? includeFile.relativePath : 'compose.yml');
2834
2935
async function handleSave() {
3036
isSaving = true;
3137
saveError = '';
3238
try {
33-
await projectService.updateProject(project.id, undefined, composeContent);
39+
if (includeFile) {
40+
await projectService.updateProjectIncludeFile(project.id, includeFile.relativePath, composeContent);
41+
} else {
42+
await projectService.updateProject(project.id, undefined, composeContent);
43+
}
3444
toast.success('Compose file saved successfully');
3545
} catch (err: any) {
3646
saveError = err?.message ?? 'Failed to save compose file';
@@ -55,7 +65,7 @@
5565

5666
<div class="bg-muted flex items-start gap-2 rounded-lg border px-4 py-3 text-sm">
5767
<span>
58-
Editing the compose file for project
68+
Editing <strong>{fileTitle}</strong> for project
5969
<a href="/projects/{project.id}" class="text-primary font-medium hover:underline"
6070
>{project.name}</a
6171
>. This container runs as the <strong>{serviceName}</strong> service.
@@ -64,12 +74,12 @@
6474

6575
<div class="flex min-h-0 flex-1 flex-col">
6676
<CodePanel
67-
title="Compose File"
77+
title={fileTitle}
6878
bind:open={panelOpen}
6979
language="yaml"
7080
bind:value={composeContent}
7181
readOnly={isReadOnly}
72-
fileId="container-compose-{project.id}"
82+
fileId="container-compose-{project.id}{includeFile ? `-${includeFile.relativePath}` : ''}"
7383
/>
7484
</div>
7585

0 commit comments

Comments
 (0)