This commit is contained in:
@@ -5,24 +5,50 @@ import {
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { Skeleton } from 'primeng/skeleton';
|
||||
import { Table, TableLazyLoadEvent, TableModule } from 'primeng/table';
|
||||
import { Column, DuckdbService, Filter, FilterValue } from '../duckdb.service';
|
||||
import {
|
||||
Table,
|
||||
TableColumnReorderEvent,
|
||||
TableLazyLoadEvent,
|
||||
TableModule,
|
||||
} from 'primeng/table';
|
||||
import {
|
||||
Aggregate,
|
||||
AggregateType,
|
||||
Column,
|
||||
DuckdbService,
|
||||
Filter,
|
||||
FilterValue,
|
||||
aggregateTypes,
|
||||
} from '../duckdb.service';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { IconField } from 'primeng/iconfield';
|
||||
import { InputIconModule } from 'primeng/inputicon';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { Menu } from 'primeng/menu';
|
||||
import { MenuItem } from 'primeng/api';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-viewer',
|
||||
standalone: true,
|
||||
imports: [TableModule, Skeleton, ButtonModule, IconField, InputIconModule],
|
||||
imports: [
|
||||
TableModule,
|
||||
Skeleton,
|
||||
ButtonModule,
|
||||
IconField,
|
||||
InputIconModule,
|
||||
InputTextModule,
|
||||
Menu,
|
||||
DecimalPipe,
|
||||
],
|
||||
template: `
|
||||
@if (file() && columns().length > 0) {
|
||||
<p-table
|
||||
#table
|
||||
sortMode="multiple"
|
||||
[reorderableColumns]="true"
|
||||
[resizableColumns]="true"
|
||||
columnResizeMode="expand"
|
||||
[columns]="columns()"
|
||||
@@ -66,6 +92,7 @@ import { InputIconModule } from 'primeng/inputicon';
|
||||
{{ col.name }}
|
||||
<p-columnFilter type="text" [field]="col.name" display="menu" />
|
||||
<p-sortIcon [field]="col.name" />
|
||||
<!-- More options can go here... integrate it with the column filter? -->
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
@@ -88,7 +115,36 @@ import { InputIconModule } from 'primeng/inputicon';
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template #footer>
|
||||
<tr>
|
||||
@for (col of columns(); track $index) {
|
||||
<td #attachment>
|
||||
@if (col.type === 'DOUBLE') {
|
||||
<div class="flex items-baseline">
|
||||
<p-button
|
||||
(click)="
|
||||
currentAggregateColumn.set($index);
|
||||
aggregateMenu.toggle($event)
|
||||
"
|
||||
[label]="aggregates()[$index]?.type?.toUpperCase()"
|
||||
>
|
||||
<ng-template #icon>
|
||||
<span class="material-symbols-outlined">
|
||||
mediation
|
||||
</span>
|
||||
</ng-template>
|
||||
</p-button>
|
||||
<span class="flex-1 text-end">
|
||||
{{ aggregateValues()[$index] | number }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
<p-menu #aggregateMenu [model]="aggregateItems" [popup]="true" />
|
||||
}
|
||||
`,
|
||||
styleUrl: './file-viewer.component.css',
|
||||
@@ -101,9 +157,24 @@ export class FileViewerComponent {
|
||||
|
||||
// Can't be computed since effect has to run first so file exists in duckdb
|
||||
protected columns = signal<Column[]>([]);
|
||||
protected aggregates = signal<(Aggregate | undefined)[]>([]);
|
||||
protected currentAggregateColumn = signal<number | undefined>(undefined);
|
||||
protected aggregateValues = signal<(number | undefined)[]>([]);
|
||||
protected numRows = 200;
|
||||
|
||||
// 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<any[]>([]);
|
||||
protected appliedFilters = signal<Filter[]>([]);
|
||||
|
||||
private table = viewChild(Table);
|
||||
|
||||
protected aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({
|
||||
label: type,
|
||||
command: () => this.updateAggregateValue(type),
|
||||
}));
|
||||
|
||||
constructor() {
|
||||
effect(async () => {
|
||||
@@ -120,15 +191,7 @@ export class FileViewerComponent {
|
||||
protected async onLazyLoad(event: TableLazyLoadEvent) {
|
||||
const file = this.file();
|
||||
if (file) {
|
||||
const rows = await this.duckdbService.getRows(
|
||||
file,
|
||||
event.first ?? 0,
|
||||
event.rows ?? 0,
|
||||
this.columns(),
|
||||
event.multiSortMeta?.map((meta) => ({
|
||||
name: meta.field,
|
||||
sortType: meta.order < 0 ? 'desc' : 'asc',
|
||||
})) ?? [],
|
||||
this.appliedFilters.set(
|
||||
event.filters
|
||||
? Object.entries(event.filters).flatMap(([column, filter]) => {
|
||||
if (Array.isArray(filter)) {
|
||||
@@ -160,7 +223,18 @@ export class FileViewerComponent {
|
||||
return [];
|
||||
})
|
||||
: [],
|
||||
[],
|
||||
);
|
||||
const rows = await this.duckdbService.getRows(
|
||||
file,
|
||||
event.first ?? 0,
|
||||
event.rows ?? 0,
|
||||
this.columns(),
|
||||
event.multiSortMeta?.map((meta) => ({
|
||||
name: meta.field,
|
||||
sortType: meta.order < 0 ? 'desc' : 'asc',
|
||||
})) ?? [],
|
||||
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) }));
|
||||
@@ -188,6 +262,7 @@ export class FileViewerComponent {
|
||||
if (file) {
|
||||
await this.duckdbService.addFile(file);
|
||||
this.columns.set(await this.duckdbService.getColumns(file));
|
||||
this.aggregates.set(new Array(this.columns().length));
|
||||
const rows = await this.duckdbService.getRows(
|
||||
file,
|
||||
0,
|
||||
@@ -200,5 +275,33 @@ export class FileViewerComponent {
|
||||
const newValue = Array.from({ length: Number(rows.totalRows) });
|
||||
this.currentValue.set(newValue);
|
||||
}
|
||||
// TODO: Replace this with previous state once persistence is implemented
|
||||
const table = this.table();
|
||||
if (table) {
|
||||
table.multiSortMeta = [{ field: this.columns()[0].name, order: 1 }];
|
||||
table.sortMultiple();
|
||||
}
|
||||
}
|
||||
|
||||
private async updateAggregateValue(type: AggregateType) {
|
||||
this.aggregates.update((aggregates) => {
|
||||
const copy = aggregates.slice();
|
||||
copy[this.currentAggregateColumn()!] = {
|
||||
type,
|
||||
column: this.columns()[this.currentAggregateColumn()!].name,
|
||||
};
|
||||
return copy;
|
||||
});
|
||||
|
||||
const aggregateValue = await this.duckdbService.getAggregateValue(
|
||||
this.file()!,
|
||||
this.appliedFilters(),
|
||||
this.aggregates()[this.currentAggregateColumn()!]!,
|
||||
);
|
||||
this.aggregateValues.update((values) => {
|
||||
const copy = values.slice();
|
||||
copy[this.currentAggregateColumn()!] = aggregateValue;
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user