Skip to content

Commit 1d55993

Browse files
committed
loading spinner
1 parent d701078 commit 1d55993

3 files changed

Lines changed: 242 additions & 8 deletions

File tree

src/ui/StackTraceApp.ts

Lines changed: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class StackgazerApp {
3838
private filterInputValue: string = '';
3939
private stackDisplayMode: 'combined' | 'side-by-side' | 'functions' | 'locations' = 'combined';
4040
private filterDebounceTimer: number | null = null;
41+
private loadingDelayTimer: number | null = null;
4142
private tooltip!: HTMLElement;
4243
private currentTheme: 'dark' | 'light' = 'dark';
4344
private initialTheme?: 'dark' | 'light';
@@ -91,6 +92,9 @@ export class StackgazerApp {
9192
ancestryEmptyState: {
9293
content: { cloneNode: (deep?: boolean) => DocumentFragment; firstElementChild: HTMLElement };
9394
};
95+
loadingOverlay: {
96+
content: { cloneNode: (deep?: boolean) => DocumentFragment; firstElementChild: HTMLElement };
97+
};
9498
};
9599

96100
constructor(containerId: string, defaultsModifier?: (defaults: AppSettings) => AppSettings) {
@@ -171,6 +175,12 @@ export class StackgazerApp {
171175
) as DocumentFragment;
172176
const settingsModalElement = settingsModalFragment.firstElementChild as HTMLElement;
173177

178+
// Create loading overlay
179+
const loadingOverlayFragment = this.templates.loadingOverlay.content.cloneNode(
180+
true
181+
) as DocumentFragment;
182+
const loadingOverlayElement = loadingOverlayFragment.firstElementChild as HTMLElement;
183+
174184
// Insert sidebar and main content into container
175185
const sidebarContainer = containerElement.querySelector('#sidebar') as HTMLElement;
176186
const mainContentContainer = containerElement.querySelector('#mainContent') as HTMLElement;
@@ -183,6 +193,7 @@ export class StackgazerApp {
183193
// Add everything to the main container
184194
this.container.appendChild(containerElement);
185195
this.container.appendChild(settingsModalElement);
196+
this.container.appendChild(loadingOverlayElement);
186197
}
187198

188199
private initializeUI(): void {
@@ -769,8 +780,27 @@ export class StackgazerApp {
769780
}
770781

771782
private async handleFiles(files: File[]): Promise<void> {
783+
const fileCount = files.length;
784+
const fileNames = files.map(f => f.name).join(', ');
785+
786+
// Show loading overlay
787+
this.showLoadingOverlay(
788+
fileCount === 1 ? 'Processing file...' : `Processing ${fileCount} files...`,
789+
fileCount === 1 ? files[0].name : fileNames
790+
);
791+
772792
try {
773-
for (const file of files) {
793+
for (let i = 0; i < files.length; i++) {
794+
const file = files[i];
795+
796+
// Update progress
797+
if (fileCount > 1) {
798+
this.updateLoadingMessage(
799+
`Processing file ${i + 1} of ${fileCount}...`,
800+
file.name
801+
);
802+
}
803+
774804
// Check if it's a zip file
775805
if (file.name.toLowerCase().endsWith('.zip') || file.type === 'application/zip') {
776806
await this.handleZipFile(file);
@@ -786,32 +816,61 @@ export class StackgazerApp {
786816
} else {
787817
console.timeEnd(`📄 File Import: ${file.name}`);
788818
console.error(`Failed to parse ${file.name}:`, result.error);
819+
this.hideLoadingOverlay();
789820
alert(`Failed to parse ${file.name}: ${result.error}`);
821+
return;
790822
}
791823
}
792824
}
793825

826+
// Update loading message for rendering
827+
this.updateLoadingMessage('Rendering results...', 'Organizing data and applying filters');
828+
794829
// Render new content but preserve filter state
795830
this.render();
796831

797832
// Always reapply current filter to ensure proper visibility state
798833
this.setFilter(this.buildCurrentFilter());
834+
835+
// Hide loading overlay
836+
this.hideLoadingOverlay();
799837
} catch (error) {
800838
console.error('Error handling files:', error);
839+
this.hideLoadingOverlay();
801840
alert(`Error handling files: ${error}`);
802841
}
803842
}
804843

805844
private async handleZipFile(file: File): Promise<void> {
806845
try {
807846
console.time(`🗜 Zip Import: ${file.name}`);
847+
848+
// Update loading message for ZIP extraction
849+
this.updateLoadingMessage('Extracting ZIP file...', `Reading ${file.name}`);
808850

809851
// Extract files using ZipHandler with settings pattern
810852
const pattern = this.settingsManager.getZipFilePatternRegex();
811853
const extractResult = await ZipHandler.extractFiles(file, pattern);
812854

813-
for (const zipFile of extractResult.files) {
855+
if (extractResult.files.length === 0) {
856+
console.warn(`No stack trace files found in zip matching pattern: ${pattern}`);
857+
throw new Error(`No stack trace files found in zip file. Looking for files matching: ${pattern}`);
858+
}
859+
860+
for (let i = 0; i < extractResult.files.length; i++) {
861+
const zipFile = extractResult.files[i];
814862
const baseName = zipFile.path.split('/').pop() || zipFile.path;
863+
864+
// Update progress for multiple files in ZIP
865+
if (extractResult.files.length > 1) {
866+
this.updateLoadingMessage(
867+
`Processing ZIP entry ${i + 1} of ${extractResult.files.length}...`,
868+
baseName
869+
);
870+
} else {
871+
this.updateLoadingMessage('Processing ZIP entry...', baseName);
872+
}
873+
815874
console.time(` 📄 Zip Entry: ${baseName}`);
816875
const result = await this.parser.parseFile(zipFile.content, baseName);
817876

@@ -821,22 +880,19 @@ export class StackgazerApp {
821880
} else {
822881
console.timeEnd(` 📄 Zip Entry: ${baseName}`);
823882
console.error(`Failed to parse ${baseName} from zip:`, result.error);
824-
alert(`Failed to parse ${baseName} from zip: ${result.error}`);
883+
throw new Error(`Failed to parse ${baseName} from zip: ${result.error}`);
825884
}
826885
}
827886

828-
if (extractResult.files.length === 0) {
829-
console.warn(`No stack trace files found in zip matching pattern: ${pattern}`);
830-
alert(`No stack trace files found in zip file. Looking for files matching: ${pattern}`);
831-
}
832887
console.timeEnd(`🗜 Zip Import: ${file.name}`);
833888
} catch (error) {
834889
console.timeEnd(`🗜 Zip Import: ${file.name}`);
835890
console.error(`Error processing zip file ${file.name}:`, error);
836-
alert(`Failed to process zip file ${file.name}: ${error}`);
891+
throw error; // Re-throw to be handled by handleFiles
837892
}
838893
}
839894

895+
840896
private debouncedSetFilter(query: string): void {
841897
// Clear any existing timer
842898
if (this.filterDebounceTimer !== null) {
@@ -2343,20 +2399,91 @@ export class StackgazerApp {
23432399
fileNameSpan.addEventListener('blur', onBlur);
23442400
}
23452401

2402+
private showLoadingOverlay(message: string, details?: string): void {
2403+
// Clear any existing delay timer
2404+
if (this.loadingDelayTimer !== null) {
2405+
clearTimeout(this.loadingDelayTimer);
2406+
this.loadingDelayTimer = null;
2407+
}
2408+
2409+
// Set up delayed showing with 300ms delay
2410+
this.loadingDelayTimer = window.setTimeout(() => {
2411+
const overlay = document.getElementById('loadingOverlay') as HTMLElement;
2412+
const messageEl = document.getElementById('loadingMessage') as HTMLElement;
2413+
const detailsEl = document.getElementById('loadingDetails') as HTMLElement;
2414+
2415+
if (overlay && messageEl) {
2416+
messageEl.textContent = message;
2417+
if (detailsEl && details) {
2418+
detailsEl.textContent = details;
2419+
detailsEl.style.display = 'block';
2420+
} else if (detailsEl) {
2421+
detailsEl.style.display = 'none';
2422+
}
2423+
overlay.style.display = 'flex';
2424+
}
2425+
this.loadingDelayTimer = null;
2426+
}, 300);
2427+
}
2428+
2429+
private hideLoadingOverlay(): void {
2430+
// Cancel the delayed showing if it hasn't triggered yet
2431+
if (this.loadingDelayTimer !== null) {
2432+
clearTimeout(this.loadingDelayTimer);
2433+
this.loadingDelayTimer = null;
2434+
}
2435+
2436+
// Hide overlay if it's already shown
2437+
const overlay = document.getElementById('loadingOverlay') as HTMLElement;
2438+
if (overlay) {
2439+
overlay.style.display = 'none';
2440+
}
2441+
}
2442+
2443+
private updateLoadingMessage(message: string, details?: string): void {
2444+
// If we have a delay timer running, cancel it and show immediately
2445+
if (this.loadingDelayTimer !== null) {
2446+
clearTimeout(this.loadingDelayTimer);
2447+
this.loadingDelayTimer = null;
2448+
2449+
// Show overlay immediately with updated message
2450+
const overlay = document.getElementById('loadingOverlay') as HTMLElement;
2451+
if (overlay) {
2452+
overlay.style.display = 'flex';
2453+
}
2454+
}
2455+
2456+
// Update the message content
2457+
const messageEl = document.getElementById('loadingMessage') as HTMLElement;
2458+
const detailsEl = document.getElementById('loadingDetails') as HTMLElement;
2459+
2460+
if (messageEl) {
2461+
messageEl.textContent = message;
2462+
}
2463+
if (detailsEl && details) {
2464+
detailsEl.textContent = details;
2465+
detailsEl.style.display = 'block';
2466+
} else if (detailsEl) {
2467+
detailsEl.style.display = 'none';
2468+
}
2469+
}
2470+
23462471
private setupDemoButtons(): void {
23472472
const demoSingleBtn = document.getElementById('demoSingleBtn');
23482473
const demoZipBtn = document.getElementById('demoZipBtn');
23492474

23502475
if (demoSingleBtn) {
23512476
demoSingleBtn.addEventListener('click', async e => {
23522477
e.preventDefault();
2478+
this.showLoadingOverlay('Loading demo file...', 'Downloading single node goroutine dump');
23532479
try {
23542480
const rawUrl =
23552481
'https://raw.githubusercontent.com/dt/crdb-stacks-examples/refs/heads/main/stacks/files/1/stacks.txt';
23562482
await this.loadFromUrl(rawUrl, 'crdb-demo-single.txt');
23572483
} catch (error) {
23582484
const msg = error && (error as any).message ? (error as any).message : String(error);
23592485
console.error('Demo file load error:', error);
2486+
this.hideLoadingOverlay();
23602487
alert(
23612488
`Failed to load demo file. Please try again or check your internet connection (${msg}).`
23622489
);
@@ -2367,12 +2494,14 @@ export class StackgazerApp {
23672494
if (demoZipBtn) {
23682495
demoZipBtn.addEventListener('click', async e => {
23692496
e.preventDefault();
2497+
this.showLoadingOverlay('Loading demo ZIP...', 'Downloading ZIP file with 4 node stack dumps');
23702498
try {
23712499
const url =
23722500
'https://raw.githubusercontent.com/dt/crdb-stacks-examples/refs/heads/main/stacks.zip';
23732501
await this.loadFromUrl(url, 'crdb-demo.zip');
23742502
} catch (error) {
23752503
console.error('Demo zip load error:', error);
2504+
this.hideLoadingOverlay();
23762505
alert(
23772506
'Failed to load demo zip file. Please try again or check your internet connection.'
23782507
);
@@ -2435,10 +2564,16 @@ export class StackgazerApp {
24352564
}
24362565
}
24372566

2567+
// Update loading message for rendering
2568+
this.updateLoadingMessage('Rendering results...', 'Organizing data and applying filters');
2569+
24382570
this.render();
24392571

24402572
// Always reapply current filter to ensure proper visibility state
24412573
this.setFilter(this.buildCurrentFilter());
2574+
2575+
// Hide loading overlay
2576+
this.hideLoadingOverlay();
24422577
} catch (error) {
24432578
// End any ongoing timers in case of error
24442579
if (fileName.endsWith('.zip')) {

src/ui/styles.css

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3027,4 +3027,88 @@ textarea.rule-warning {
30273027

30283028
.copy-notification.show {
30293029
opacity: 1;
3030+
}
3031+
3032+
/* Loading spinner and overlay */
3033+
.loading-overlay {
3034+
position: fixed;
3035+
top: 0;
3036+
left: 0;
3037+
width: 100%;
3038+
height: 100%;
3039+
background: rgba(0, 0, 0, 0.7);
3040+
display: flex;
3041+
align-items: center;
3042+
justify-content: center;
3043+
z-index: 10001;
3044+
pointer-events: auto;
3045+
}
3046+
3047+
.loading-spinner {
3048+
width: 60px;
3049+
height: 60px;
3050+
border: 4px solid var(--bg-tertiary);
3051+
border-top: 4px solid var(--accent-primary);
3052+
border-radius: 50%;
3053+
animation: spin 1s linear infinite;
3054+
}
3055+
3056+
.loading-content {
3057+
display: flex;
3058+
flex-direction: column;
3059+
align-items: center;
3060+
gap: 20px;
3061+
color: var(--text-primary);
3062+
background: var(--bg-secondary);
3063+
padding: 30px;
3064+
border-radius: 12px;
3065+
border: 1px solid var(--border-color);
3066+
min-width: 300px;
3067+
text-align: center;
3068+
}
3069+
3070+
.loading-message {
3071+
font-size: 16px;
3072+
font-weight: 500;
3073+
}
3074+
3075+
.loading-details {
3076+
font-size: 14px;
3077+
color: var(--text-secondary);
3078+
opacity: 0.8;
3079+
}
3080+
3081+
.loading-progress {
3082+
width: 100%;
3083+
max-width: 250px;
3084+
height: 8px;
3085+
background: var(--bg-tertiary);
3086+
border-radius: 4px;
3087+
overflow: hidden;
3088+
}
3089+
3090+
.loading-progress-bar {
3091+
height: 100%;
3092+
background: var(--accent-primary);
3093+
border-radius: 4px;
3094+
width: 0%;
3095+
transition: width 0.3s ease;
3096+
}
3097+
3098+
.loading-progress-indeterminate {
3099+
height: 100%;
3100+
background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
3101+
border-radius: 4px;
3102+
width: 50%;
3103+
animation: loading-slide 2s ease-in-out infinite;
3104+
}
3105+
3106+
@keyframes spin {
3107+
0% { transform: rotate(0deg); }
3108+
100% { transform: rotate(360deg); }
3109+
}
3110+
3111+
@keyframes loading-slide {
3112+
0% { transform: translateX(-100%); }
3113+
100% { transform: translateX(200%); }
30303114
}

src/ui/templates.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,20 @@ const AncestryEmptyStateTemplate = (
305305
<p className="ancestry-empty-state">No ancestry data available</p>
306306
);
307307

308+
// Loading Overlay Template
309+
const LoadingOverlayTemplate = (
310+
<div className="loading-overlay" id="loadingOverlay" style="display: none;">
311+
<div className="loading-content">
312+
<div className="loading-spinner"></div>
313+
<div className="loading-message" id="loadingMessage">Processing files...</div>
314+
<div className="loading-details" id="loadingDetails"></div>
315+
<div className="loading-progress" id="loadingProgressContainer" style="display: none;">
316+
<div className="loading-progress-indeterminate" id="loadingProgressBar"></div>
317+
</div>
318+
</div>
319+
</div>
320+
);
321+
308322
// Helper function to create a unified setting component
309323
function createSettingComponent(config: {
310324
id: string;
@@ -476,6 +490,7 @@ export const templates = {
476490
settingsModal: createTemplateFromElement(SettingsModalTemplate),
477491
ancestryModal: createTemplateFromElement(AncestryModalTemplate),
478492
ancestryEmptyState: createTemplateFromElement(AncestryEmptyStateTemplate),
493+
loadingOverlay: createTemplateFromElement(LoadingOverlayTemplate),
479494
};
480495

481496
// Export helper functions for programmatic modal generation

0 commit comments

Comments
 (0)