Skip to content

Commit 661df84

Browse files
authored
Merge pull request #1117 from terrateamio/1116-fix-drift-pagination
#1116 REFACTOR analytics drift detection pagination
2 parents 9fdea7c + a3f70e4 commit 661df84

File tree

3 files changed

+127
-69
lines changed

3 files changed

+127
-69
lines changed

code/src/iris/src/lib/Analytics.svelte

Lines changed: 123 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import type { Dirspace, Repository } from './types';
1111
import { navigateToRun, navigateToRuns } from './utils/navigation';
1212
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+
1317
// Tab state
1418
let activeTab: 'repository' | 'workflow' | 'drift' = 'repository';
1519
@@ -129,9 +133,12 @@
129133
let expandedWorkflowStep: string | null = null;
130134
let expandedDriftItem: string | null = null;
131135
132-
// Drift Analytics state
136+
// Drift Analytics state
133137
let driftOperations: Dirspace[] = [];
134138
let isLoadingDrift = false;
139+
let isLoadingMoreDrift = false;
140+
let hasMoreDrift = false;
141+
let nextDriftPageUrl: string | null = null;
135142
let driftError: string | null = null;
136143
let driftMetrics = {
137144
totalDrifts: 0,
@@ -266,86 +273,114 @@
266273
}
267274
}
268275
269-
async function loadDriftOperations(): Promise<void> {
276+
async function loadDriftOperations(loadMore: boolean = false): Promise<void> {
270277
if (!$selectedInstallation) return;
271278
272-
isLoadingDrift = true;
279+
if (loadMore) {
280+
isLoadingMoreDrift = true;
281+
} else {
282+
isLoadingDrift = true;
283+
hasMoreDrift = false;
284+
nextDriftPageUrl = null;
285+
}
273286
driftError = null;
274-
287+
275288
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
282289
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];
308318
}
309319
}
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-
});
322320
}
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`);
326348
}
327-
328-
pagesLoaded++;
329-
349+
330350
// 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;
334354
} 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 = [];
336362
}
337363
}
338-
339-
driftOperations = allDriftOperations;
364+
340365
} catch (err) {
341366
console.error('❌ Error loading drift operations:', err);
342367
driftError = err instanceof Error ? err.message : 'Failed to load drift operations';
343-
driftOperations = [];
368+
if (!loadMore) {
369+
driftOperations = [];
370+
}
344371
} finally {
345-
isLoadingDrift = false;
372+
if (loadMore) {
373+
isLoadingMoreDrift = false;
374+
} else {
375+
isLoadingDrift = false;
376+
}
346377
}
347378
}
348379
380+
async function loadMoreDrift(): Promise<void> {
381+
await loadDriftOperations(true);
382+
}
383+
349384
function calculateDriftMetrics(): void {
350385
if (driftOperations.length === 0) {
351386
driftMetrics = {
@@ -1647,8 +1682,8 @@
16471682
<p class="text-lg font-medium">Failed to Load Drift Data</p>
16481683
<p class="text-sm mt-1">{driftError}</p>
16491684
</div>
1650-
<button
1651-
on:click={loadDriftOperations}
1685+
<button
1686+
on:click={() => loadDriftOperations()}
16521687
class="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
16531688
>
16541689
Retry Loading
@@ -1681,12 +1716,12 @@
16811716
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Based on recent drift operations</p>
16821717
</div>
16831718
<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' : ''}
16851720
</div>
16861721
</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}
16901725
<div class="bg-gray-50 dark:bg-gray-700 rounded-md">
16911726
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 md:p-4 gap-3">
16921727
<div class="flex-1 min-w-0">
@@ -1895,6 +1930,27 @@
18951930
</div>
18961931
{/each}
18971932
</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}
18981954
</Card>
18991955
{/if}
19001956
{/if}

code/src/iris/src/lib/Stacks.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import { onMount } from 'svelte';
1111
1212
// Route params (provided by router, may be unused)
13+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1314
export let params: { installationId?: string } = {};
1415
1516
// Tab management
@@ -221,7 +222,7 @@
221222

222223
<!-- Tab Navigation -->
223224
<div class="border-b border-gray-200 dark:border-gray-700">
224-
<nav class="-mb-px flex space-x-8" aria-label="Tabs" role="tablist">
225+
<div class="-mb-px flex space-x-8" role="tablist" aria-label="Tabs">
225226
<button
226227
on:click={() => setActiveTab('dashboard')}
227228
class="py-2 px-1 border-b-2 font-medium text-sm transition-colors duration-200 {activeTab === 'dashboard'
@@ -266,7 +267,7 @@
266267
>
267268
Timeline
268269
</button>
269-
</nav>
270+
</div>
270271
</div>
271272

272273
<!-- Tab Panels -->

code/src/iris/src/lib/components/stacks/StacksPRsView.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
export let loadErrors: Array<{ prNumber: number; error: string }>;
1010
export let searchQuery: string;
1111
export let repoFilter: string;
12+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1213
export let uniqueRepos: string[];
1314
export let timeRange: number;
1415
export let onRefresh: () => void;

0 commit comments

Comments
 (0)