diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 3687957..0e1e02c 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,7 +1,6 @@ import { ApplicationConfig, - provideAppInitializer, - provideZoneChangeDetection, + provideZonelessChangeDetection, } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; @@ -12,7 +11,7 @@ import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), + provideZonelessChangeDetection(), provideRouter(routes), provideAnimationsAsync(), providePrimeNG({ diff --git a/src/app/file-viewer/file-viewer.component.css b/src/app/file-viewer/file-viewer.component.css index 478abc9..dd67487 100644 --- a/src/app/file-viewer/file-viewer.component.css +++ b/src/app/file-viewer/file-viewer.component.css @@ -1,4 +1,5 @@ :host { - display: block; + display: flex; + flex-direction: column; height: 100%; } diff --git a/src/app/file-viewer/file-viewer.component.ts b/src/app/file-viewer/file-viewer.component.ts index 58f6e71..749639d 100644 --- a/src/app/file-viewer/file-viewer.component.ts +++ b/src/app/file-viewer/file-viewer.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, effect, inject, @@ -30,6 +31,7 @@ import { InputTextModule } from 'primeng/inputtext'; import { Menu } from 'primeng/menu'; import { MenuItem } from 'primeng/api'; import { DecimalPipe } from '@angular/common'; +import { PaginatorModule, PaginatorState } from 'primeng/paginator'; @Component({ selector: 'app-file-viewer', @@ -43,107 +45,123 @@ import { DecimalPipe } from '@angular/common'; InputTextModule, Menu, DecimalPipe, + PaginatorModule, ], template: ` @if (file() && columns().length > 0) { - - -
- - - - - - - -
-
- - - @for (col of columns; track $index) { - - {{ col.name }} - - - - - } - - - - - @for (col of columns; track $index) { - - {{ rowData[col.name] }} - - } - - - - - @for (col of columns; track $index) { - - - - } - - - - - @for (col of columns(); track $index) { - - @if (col.type === 'DOUBLE') { -
- - - - mediation - - - - - {{ aggregateValues()[$index] | number }} - -
- } - - } - -
-
+
+ + +
+ + + + + + + +
+
+ + + @for (col of columns; track $index) { + + {{ col.name }} + + + + + } + + + + + @for (col of columns; track $index) { + + {{ rowData[col.name] }} + + } + + + + + @for (col of columns; track $index) { + + + + } + + + + + @for (col of columns(); track $index) { + + @if (col.type === 'DOUBLE' || col.type === 'BIGINT') { +
+ + + + mediation + + + + + {{ aggregateValues()[$index] | number }} + +
+ } + + } + +
+
+
+ @if (currentRowCount() > PAGE_SIZE) { + + + } } `, @@ -160,14 +178,16 @@ export class FileViewerComponent { protected aggregates = signal<(Aggregate | undefined)[]>([]); protected currentAggregateColumn = signal(undefined); protected aggregateValues = signal<(number | undefined)[]>([]); - protected numRows = 200; + protected readonly NUM_ROWS = 200; + protected currentPage = signal(0); + + // This is required since once the number of rows exceeds ~300000, the footer gets blocked + // and the user can't even scroll to those bottom rows. + protected readonly PAGE_SIZE = 300_000; - // TODO: This needs to be limited to 500000 or something, since exceeding the max safe integer - // will cause weird layout and prevent the bottom rows/footer from being viewed. Alternately - // we can use paging instead, but allow paging to be virtual with a max page size - // of 500000 protected currentValue = signal([]); protected appliedFilters = signal([]); + protected currentRowCount = signal(0n); private table = viewChild(Table); @@ -226,7 +246,7 @@ export class FileViewerComponent { ); const rows = await this.duckdbService.getRows( file, - event.first ?? 0, + (event.first ?? 0) + this.currentPage() * this.PAGE_SIZE, event.rows ?? 0, this.columns(), event.multiSortMeta?.map((meta) => ({ @@ -236,23 +256,21 @@ export class FileViewerComponent { this.appliedFilters(), this.aggregates().filter((agg) => !!agg) as Aggregate[], ); - if (this.currentValue().length !== Number(rows.totalRows)) { - this.currentValue.set(Array.from({ length: Number(rows.totalRows) })); - return; - } // First clear out existing data, don't want to risk loading entire file into memory // Keep data in previous/next page so when user scrolls between 2 pages they don't see missing data for (let i = 0; i < this.currentValue().length; i++) { if ( - i < event.first! - this.numRows || - i > event.first! + event.rows! + this.numRows + i < event.first! - this.NUM_ROWS || + i > event.first! + event.rows! + this.NUM_ROWS ) { this.currentValue()[i] = undefined; } } - // Can't update the current value, otherwise we get an infinite loop - this.currentValue().splice(event.first!, event.rows!, ...rows.rows); + // Can't update the current value, otherwise we get an infinite loop due to primeng change detection rerunning (plus it's faster to mutate) + for (let i = 0; i < event.rows!; i++) { + this.currentValue()[event.first! + i] = rows.rows![i]; + } event.forceUpdate!(); } } @@ -266,13 +284,19 @@ export class FileViewerComponent { const rows = await this.duckdbService.getRows( file, 0, - this.numRows, + this.NUM_ROWS, this.columns(), [], [], [], ); - const newValue = Array.from({ length: Number(rows.totalRows) }); + this.currentRowCount.set(rows.totalRows); + this.currentPage.set(0); + const newValue = Array.from({ + length: Number( + rows.totalRows > this.PAGE_SIZE ? this.PAGE_SIZE : rows.totalRows, + ), + }); this.currentValue.set(newValue); } // TODO: Replace this with previous state once persistence is implemented @@ -304,4 +328,16 @@ export class FileViewerComponent { return copy; }); } + + protected onPageChange(event: PaginatorState) { + this.currentPage.set(event.page!); + this.currentValue.set( + Array.from({ + length: + event.page! === event.pageCount! - 1 + ? Number(this.currentRowCount()) % this.PAGE_SIZE + : this.PAGE_SIZE, + }), + ); + } }