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,
+ }),
+ );
+ }
}