|
10 | 10 | import type { Dirspace, Repository } from './types'; |
11 | 11 | import { navigateToRun, navigateToRuns } from './utils/navigation'; |
12 | 12 |
|
| 13 | + // Router params (from svelte-spa-router, unused but required by router interface) |
| 14 | + // eslint-disable-next-line @typescript-eslint/no-unused-vars |
| 15 | + export let params: Record<string, string> = {}; |
| 16 | +
|
13 | 17 | // Tab state |
14 | 18 | let activeTab: 'repository' | 'workflow' | 'drift' = 'repository'; |
15 | 19 |
|
|
129 | 133 | let expandedWorkflowStep: string | null = null; |
130 | 134 | let expandedDriftItem: string | null = null; |
131 | 135 |
|
132 | | - // Drift Analytics state |
| 136 | + // Drift Analytics state |
133 | 137 | let driftOperations: Dirspace[] = []; |
134 | 138 | let isLoadingDrift = false; |
| 139 | + let isLoadingMoreDrift = false; |
| 140 | + let hasMoreDrift = false; |
| 141 | + let nextDriftPageUrl: string | null = null; |
135 | 142 | let driftError: string | null = null; |
136 | 143 | let driftMetrics = { |
137 | 144 | totalDrifts: 0, |
|
266 | 273 | } |
267 | 274 | } |
268 | 275 |
|
269 | | - async function loadDriftOperations(): Promise<void> { |
| 276 | + async function loadDriftOperations(loadMore: boolean = false): Promise<void> { |
270 | 277 | if (!$selectedInstallation) return; |
271 | 278 |
|
272 | | - isLoadingDrift = true; |
| 279 | + if (loadMore) { |
| 280 | + isLoadingMoreDrift = true; |
| 281 | + } else { |
| 282 | + isLoadingDrift = true; |
| 283 | + hasMoreDrift = false; |
| 284 | + nextDriftPageUrl = null; |
| 285 | + } |
273 | 286 | driftError = null; |
274 | | - |
| 287 | +
|
275 | 288 | try { |
276 | | - // Load a reasonable sample for analytics (up to 5 pages / 250 drift operations) |
277 | | - const allDriftOperations: Dirspace[] = []; |
278 | | - let hasMore = true; |
279 | | - let nextPageUrl: string | null = null; |
280 | | - let pagesLoaded = 0; |
281 | | - const maxPages = 5; // Fewer pages for drift since it's less common |
282 | 289 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; |
283 | | - |
284 | | - while (hasMore && pagesLoaded < maxPages) { |
285 | | - let response; |
286 | | - |
287 | | - if (nextPageUrl) { |
288 | | - // Use the URL from Link header directly |
289 | | - const fetchResponse: Response = await fetch(nextPageUrl, { |
290 | | - method: 'GET', |
291 | | - headers: { 'Content-Type': 'application/json' }, |
292 | | - credentials: 'include', |
293 | | - }); |
294 | | - |
295 | | - const rawResponse: { dirspaces: Dirspace[] } = await fetchResponse.json(); |
296 | | - |
297 | | - // Parse Link headers from the response |
298 | | - const linkHeader = fetchResponse.headers.get('Link'); |
299 | | - let linkHeaders: Record<string, string> | null = null; |
300 | | - if (linkHeader) { |
301 | | - linkHeaders = {}; |
302 | | - const parts = linkHeader.split(/,\s*(?=<)/); |
303 | | - for (const part of parts) { |
304 | | - const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/); |
305 | | - if (match) { |
306 | | - linkHeaders[match[2]] = match[1]; |
307 | | - } |
| 290 | + let response; |
| 291 | +
|
| 292 | + if (loadMore && nextDriftPageUrl) { |
| 293 | + console.log(`📄 Loading more drift operations (page)...`); |
| 294 | +
|
| 295 | + // Use the URL from Link header directly |
| 296 | + const fetchResponse: Response = await fetch(nextDriftPageUrl, { |
| 297 | + method: 'GET', |
| 298 | + headers: { 'Content-Type': 'application/json' }, |
| 299 | + credentials: 'include', |
| 300 | + }); |
| 301 | +
|
| 302 | + if (!fetchResponse.ok) { |
| 303 | + throw new Error(`HTTP ${fetchResponse.status}: ${fetchResponse.statusText}`); |
| 304 | + } |
| 305 | +
|
| 306 | + const rawResponse: { dirspaces: Dirspace[] } = await fetchResponse.json(); |
| 307 | +
|
| 308 | + // Parse Link headers from the response |
| 309 | + const linkHeader = fetchResponse.headers.get('Link'); |
| 310 | + let linkHeaders: Record<string, string> | null = null; |
| 311 | + if (linkHeader) { |
| 312 | + linkHeaders = {}; |
| 313 | + const parts = linkHeader.split(/,\s*(?=<)/); |
| 314 | + for (const part of parts) { |
| 315 | + const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/); |
| 316 | + if (match) { |
| 317 | + linkHeaders[match[2]] = match[1]; |
308 | 318 | } |
309 | 319 | } |
310 | | - |
311 | | - response = { |
312 | | - dirspaces: rawResponse.dirspaces || [], |
313 | | - linkHeaders |
314 | | - }; |
315 | | - } else { |
316 | | - // Initial request |
317 | | - response = await api.getInstallationDirspaces($selectedInstallation.id, { |
318 | | - q: 'kind:drift', |
319 | | - tz: timezone, |
320 | | - limit: 50 |
321 | | - }); |
322 | 320 | } |
323 | | - |
324 | | - if (response && response.dirspaces) { |
325 | | - allDriftOperations.push(...response.dirspaces); |
| 321 | +
|
| 322 | + response = { |
| 323 | + dirspaces: rawResponse.dirspaces || [], |
| 324 | + linkHeaders |
| 325 | + }; |
| 326 | + } else { |
| 327 | + console.log(`📊 Loading initial drift operations...`); |
| 328 | +
|
| 329 | + // Initial request - load first batch |
| 330 | + response = await api.getInstallationDirspaces($selectedInstallation.id, { |
| 331 | + q: 'kind:drift', |
| 332 | + tz: timezone, |
| 333 | + limit: 50 |
| 334 | + }); |
| 335 | + } |
| 336 | +
|
| 337 | + if (response && response.dirspaces) { |
| 338 | + const newDrifts = response.dirspaces as Dirspace[]; |
| 339 | +
|
| 340 | + if (loadMore) { |
| 341 | + // Append to existing results |
| 342 | + driftOperations = [...driftOperations, ...newDrifts]; |
| 343 | + console.log(`✅ Loaded ${newDrifts.length} more drift operations (total: ${driftOperations.length})`); |
| 344 | + } else { |
| 345 | + // Replace results for initial load |
| 346 | + driftOperations = newDrifts; |
| 347 | + console.log(`✅ Loaded ${newDrifts.length} drift operations initially`); |
326 | 348 | } |
327 | | - |
328 | | - pagesLoaded++; |
329 | | - |
| 349 | +
|
330 | 350 | // Check for next page |
331 | | - if (response.linkHeaders?.next && pagesLoaded < maxPages) { |
332 | | - nextPageUrl = response.linkHeaders.next.replace('//api/', '/api/'); |
333 | | - hasMore = true; |
| 351 | + if (response.linkHeaders?.next) { |
| 352 | + nextDriftPageUrl = response.linkHeaders.next.replace('//api/', '/api/'); |
| 353 | + hasMoreDrift = true; |
334 | 354 | } else { |
335 | | - hasMore = false; |
| 355 | + nextDriftPageUrl = null; |
| 356 | + hasMoreDrift = false; |
| 357 | + console.log(`✅ All drift operations loaded (total: ${driftOperations.length})`); |
| 358 | + } |
| 359 | + } else { |
| 360 | + if (!loadMore) { |
| 361 | + driftOperations = []; |
336 | 362 | } |
337 | 363 | } |
338 | | - |
339 | | - driftOperations = allDriftOperations; |
| 364 | +
|
340 | 365 | } catch (err) { |
341 | 366 | console.error('❌ Error loading drift operations:', err); |
342 | 367 | driftError = err instanceof Error ? err.message : 'Failed to load drift operations'; |
343 | | - driftOperations = []; |
| 368 | + if (!loadMore) { |
| 369 | + driftOperations = []; |
| 370 | + } |
344 | 371 | } finally { |
345 | | - isLoadingDrift = false; |
| 372 | + if (loadMore) { |
| 373 | + isLoadingMoreDrift = false; |
| 374 | + } else { |
| 375 | + isLoadingDrift = false; |
| 376 | + } |
346 | 377 | } |
347 | 378 | } |
348 | 379 |
|
| 380 | + async function loadMoreDrift(): Promise<void> { |
| 381 | + await loadDriftOperations(true); |
| 382 | + } |
| 383 | +
|
349 | 384 | function calculateDriftMetrics(): void { |
350 | 385 | if (driftOperations.length === 0) { |
351 | 386 | driftMetrics = { |
|
1647 | 1682 | <p class="text-lg font-medium">Failed to Load Drift Data</p> |
1648 | 1683 | <p class="text-sm mt-1">{driftError}</p> |
1649 | 1684 | </div> |
1650 | | - <button |
1651 | | - on:click={loadDriftOperations} |
| 1685 | + <button |
| 1686 | + on:click={() => loadDriftOperations()} |
1652 | 1687 | class="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700" |
1653 | 1688 | > |
1654 | 1689 | Retry Loading |
|
1681 | 1716 | <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Based on recent drift operations</p> |
1682 | 1717 | </div> |
1683 | 1718 | <div class="text-xs text-gray-600 dark:text-gray-400 text-left sm:text-right"> |
1684 | | - Showing {Math.min(20, driftOperations.length)} of {driftOperations.length} drift operations |
| 1719 | + Showing {driftOperations.length} drift operation{driftOperations.length !== 1 ? 's' : ''} |
1685 | 1720 | </div> |
1686 | 1721 | </div> |
1687 | | - |
1688 | | - <div class="space-y-3 md:space-y-4 max-h-96 overflow-y-auto"> |
1689 | | - {#each driftOperations.slice(0, 20) as drift} |
| 1722 | + |
| 1723 | + <div class="space-y-3 md:space-y-4 max-h-[calc(100vh-400px)] overflow-y-auto"> |
| 1724 | + {#each driftOperations as drift} |
1690 | 1725 | <div class="bg-gray-50 dark:bg-gray-700 rounded-md"> |
1691 | 1726 | <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 md:p-4 gap-3"> |
1692 | 1727 | <div class="flex-1 min-w-0"> |
|
1895 | 1930 | </div> |
1896 | 1931 | {/each} |
1897 | 1932 | </div> |
| 1933 | + |
| 1934 | + <!-- Load More Button --> |
| 1935 | + {#if hasMoreDrift} |
| 1936 | + <div class="mt-4 flex justify-center"> |
| 1937 | + <button |
| 1938 | + on:click={loadMoreDrift} |
| 1939 | + disabled={isLoadingMoreDrift} |
| 1940 | + class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" |
| 1941 | + > |
| 1942 | + {#if isLoadingMoreDrift} |
| 1943 | + <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-gray-600 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> |
| 1944 | + <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> |
| 1945 | + <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
| 1946 | + </svg> |
| 1947 | + Loading more... |
| 1948 | + {:else} |
| 1949 | + Load More Drift Operations |
| 1950 | + {/if} |
| 1951 | + </button> |
| 1952 | + </div> |
| 1953 | + {/if} |
1898 | 1954 | </Card> |
1899 | 1955 | {/if} |
1900 | 1956 | {/if} |
|
0 commit comments