|
41 | 41 | } from '$lib/icons'; |
42 | 42 | import { parse as parseYaml } from 'yaml'; |
43 | 43 | import type { IncludeFile } from '$lib/types/project.type'; |
| 44 | + import { projectService } from '$lib/services/project-service'; |
| 45 | + import { environmentStore } from '$lib/stores/environment.store.svelte'; |
44 | 46 | let { data } = $props(); |
45 | 47 | let container = $derived(data?.container as ContainerDetailsDto); |
46 | 48 | let stats = $state(null as ContainerStatsType | null); |
|
150 | 152 | // Find which file (root compose or an include file) directly defines this service. |
151 | 153 | // Returns { includeFile: null } for root compose, { includeFile: <file> } for a sub-file, |
152 | 154 | // or null if the service isn't found anywhere (hides the tab). |
153 | | - const serviceComposeSource = $derived( |
154 | | - (() => { |
155 | | - if (!project || !composeServiceName || !composeInfo) return null; |
156 | | -
|
157 | | - const hasService = (content: string): boolean => { |
158 | | - try { |
159 | | - const parsed = parseYaml(content) as Record<string, unknown> | null; |
160 | | - return !!(parsed?.services && (parsed.services as Record<string, unknown>)[composeServiceName]); |
161 | | - } catch { |
162 | | - return false; |
163 | | - } |
164 | | - }; |
| 155 | + // |
| 156 | + // Include file content is lazy-loaded (PR #2259), so we fetch on-demand via |
| 157 | + // getProjectFileForEnvironment, stopping as soon as the service is found. |
| 158 | + const hasServiceInContent = (content: string, serviceName: string): boolean => { |
| 159 | + try { |
| 160 | + const parsed = parseYaml(content) as Record<string, unknown> | null; |
| 161 | + return !!(parsed?.services && (parsed.services as Record<string, unknown>)[serviceName]); |
| 162 | + } catch { |
| 163 | + return false; |
| 164 | + } |
| 165 | + }; |
165 | 166 |
|
166 | | - if (project.composeContent && hasService(project.composeContent)) { |
167 | | - return { includeFile: null as IncludeFile | null }; |
168 | | - } |
| 167 | + async function resolveServiceComposeSource( |
| 168 | + proj: typeof project, |
| 169 | + svcName: string, |
| 170 | + info: typeof composeInfo |
| 171 | + ): Promise<{ includeFile: IncludeFile | null } | null> { |
| 172 | + if (!proj || !svcName || !info) return null; |
| 173 | +
|
| 174 | + // Check root compose first (content is always present) |
| 175 | + if (proj.composeContent && hasServiceInContent(proj.composeContent, svcName)) { |
| 176 | + return { includeFile: null }; |
| 177 | + } |
| 178 | +
|
| 179 | + // Lazy-fetch include file contents one at a time until we find the service |
| 180 | + const includes = proj.includeFiles ?? []; |
| 181 | + if (includes.length === 0) return null; |
| 182 | +
|
| 183 | + const envId = await environmentStore.getCurrentEnvironmentId().catch(() => null); |
| 184 | + if (!envId) return null; |
169 | 185 |
|
170 | | - for (const f of project.includeFiles ?? []) { |
171 | | - if (hasService(f.content)) { |
172 | | - return { includeFile: f }; |
| 186 | + for (const f of includes) { |
| 187 | + if (f.content && hasServiceInContent(f.content, svcName)) { |
| 188 | + return { includeFile: f }; |
| 189 | + } |
| 190 | + try { |
| 191 | + const loaded = await projectService.getProjectFileForEnvironment(envId, proj.id, f.relativePath); |
| 192 | + if (loaded?.content && hasServiceInContent(loaded.content, svcName)) { |
| 193 | + return { includeFile: { ...f, content: loaded.content } }; |
173 | 194 | } |
| 195 | + } catch { |
| 196 | + // Skip files that fail to load |
174 | 197 | } |
| 198 | + } |
| 199 | + return null; |
| 200 | + } |
175 | 201 |
|
176 | | - return null; |
177 | | - })() |
| 202 | + const serviceComposeSourcePromise = $derived( |
| 203 | + resolveServiceComposeSource(project, composeServiceName, composeInfo) |
178 | 204 | ); |
179 | 205 |
|
180 | | - const showComposeTab = $derived(!!composeInfo && !!serviceComposeSource); |
| 206 | + const showComposeTab = $derived(!!composeInfo && !!project); |
181 | 207 |
|
182 | 208 | const tabItems = $derived<TabItem[]>([ |
183 | 209 | { value: 'overview', label: m.common_overview(), icon: ContainersIcon }, |
|
335 | 361 | </Tabs.Content> |
336 | 362 | {/if} |
337 | 363 |
|
338 | | - {#if project && serviceComposeSource} |
339 | | - <Tabs.Content value="compose" class="h-full min-h-0"> |
340 | | - {#key `${project?.id}-${serviceComposeSource?.includeFile?.relativePath ?? 'root'}`} |
341 | | - <ContainerComposePanel |
342 | | - {project} |
343 | | - serviceName={composeServiceName} |
344 | | - includeFile={serviceComposeSource.includeFile} |
345 | | - rootFilename={rootComposeFilename} |
346 | | - /> |
347 | | - {/key} |
348 | | - </Tabs.Content> |
349 | | - {/if} |
| 364 | + {#await serviceComposeSourcePromise then serviceComposeSource} |
| 365 | + {#if project && serviceComposeSource} |
| 366 | + <Tabs.Content value="compose" class="h-full min-h-0"> |
| 367 | + {#key `${project?.id}-${serviceComposeSource?.includeFile?.relativePath ?? 'root'}`} |
| 368 | + <ContainerComposePanel |
| 369 | + {project} |
| 370 | + serviceName={composeServiceName} |
| 371 | + includeFile={serviceComposeSource.includeFile} |
| 372 | + rootFilename={rootComposeFilename} |
| 373 | + /> |
| 374 | + {/key} |
| 375 | + </Tabs.Content> |
| 376 | + {/if} |
| 377 | + {/await} |
350 | 378 | {/snippet} |
351 | 379 | </TabbedPageLayout> |
352 | 380 | {:else} |
|
0 commit comments