This commit is contained in:
@@ -11,12 +11,7 @@
|
|||||||
<p-button icon="pi pi-save" class="mr-2" text severity="secondary" />
|
<p-button icon="pi pi-save" class="mr-2" text severity="secondary" />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</p-toolbar>
|
</p-toolbar>
|
||||||
<p-splitter
|
<p-splitter [panelSizes]="[15, 70, 15]" class="h-full" styleClass="mb-8 h-full">
|
||||||
[panelSizes]="[15, 70, 15]"
|
|
||||||
class="h-full"
|
|
||||||
[style]="{ height: '100%' }"
|
|
||||||
styleClass="mb-8"
|
|
||||||
>
|
|
||||||
<ng-template #panel>
|
<ng-template #panel>
|
||||||
<div class="h-full w-full">
|
<div class="h-full w-full">
|
||||||
<app-file-tree (selectFile)="selectedFile.set($event)"></app-file-tree>
|
<app-file-tree (selectFile)="selectedFile.set($event)"></app-file-tree>
|
||||||
@@ -25,18 +20,31 @@
|
|||||||
<ng-template #panel>
|
<ng-template #panel>
|
||||||
<p-splitter layout="vertical" [panelSizes]="[70, 30]">
|
<p-splitter layout="vertical" [panelSizes]="[70, 30]">
|
||||||
<ng-template #panel>
|
<ng-template #panel>
|
||||||
<p-tabs value="0" class="w-full"
|
<div class="flex flex-col w-full">
|
||||||
><p-tablist>
|
@if (tabs().length > 0) {
|
||||||
<p-tab value="0">Tab 1</p-tab>
|
<p-tabs [(value)]="selectedTab" class="w-full" scrollable>
|
||||||
<p-tab value="1">Tab 2</p-tab>
|
<p-tablist>
|
||||||
</p-tablist>
|
@for (tab of tabs(); track $index) {
|
||||||
<p-tabpanels class="h-full">
|
<p-tab [value]="$index">
|
||||||
<p-tabpanel value="0">
|
<span>{{ tab.name }}</span>
|
||||||
<app-file-viewer [file]="selectedFile()"></app-file-viewer>
|
<span
|
||||||
</p-tabpanel>
|
(click)="removeTab($index)"
|
||||||
</p-tabpanels>
|
class="material-symbols-outlined"
|
||||||
</p-tabs>
|
>
|
||||||
<!-- <div class="col flex items-center justify-center">Panel 2</div> -->
|
close
|
||||||
|
</span>
|
||||||
|
</p-tab>
|
||||||
|
}
|
||||||
|
</p-tablist>
|
||||||
|
</p-tabs>
|
||||||
|
}
|
||||||
|
@if (selectedFile()) {
|
||||||
|
<app-file-viewer
|
||||||
|
class="flex w-full flex-1"
|
||||||
|
[file]="selectedFile()"
|
||||||
|
></app-file-viewer>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #panel>
|
<ng-template #panel>
|
||||||
<div class="col flex items-center justify-center">
|
<div class="col flex items-center justify-center">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, signal, viewChild } from '@angular/core';
|
import { Component, effect, signal, viewChild } from '@angular/core';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { SplitterModule } from 'primeng/splitter';
|
import { SplitterModule } from 'primeng/splitter';
|
||||||
import { TabsModule } from 'primeng/tabs';
|
import { TabsModule } from 'primeng/tabs';
|
||||||
@@ -28,7 +28,68 @@ import { FileViewerComponent } from './file-viewer/file-viewer.component';
|
|||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
// TODO: When a file is selected, we add a new sheet with that file as the current file?
|
protected selectedFile = signal<File | undefined>(undefined);
|
||||||
// For now we'll just store the current file only
|
protected tabs = signal<File[]>([]);
|
||||||
selectedFile = signal<File | undefined>(undefined);
|
protected selectedTab = signal(0);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
const selectedFile = this.selectedFile();
|
||||||
|
if (selectedFile) {
|
||||||
|
if (
|
||||||
|
this.tabs().find(
|
||||||
|
(tab) => tab.webkitRelativePath === selectedFile.webkitRelativePath,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.selectedTab.set(
|
||||||
|
this.tabs().findIndex(
|
||||||
|
(tab) =>
|
||||||
|
tab.webkitRelativePath === selectedFile.webkitRelativePath,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.tabs.update((tabs) => [...tabs, selectedFile]);
|
||||||
|
this.selectedTab.set(this.tabs().length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
if (this.selectedFile() !== this.tabs()[this.selectedTab()]) {
|
||||||
|
if (this.tabs().length > 0) {
|
||||||
|
this.selectedFile.set(this.tabs()[Number(this.selectedTab())]);
|
||||||
|
} else {
|
||||||
|
this.selectedFile.set(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeTab(index: number) {
|
||||||
|
this.tabs.update((tabs) => {
|
||||||
|
const copy = tabs.slice();
|
||||||
|
copy.splice(index);
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
if (this.selectedTab() === index) {
|
||||||
|
if (this.selectedTab() > 0) {
|
||||||
|
this.selectedTab.update((tab) => tab - 1);
|
||||||
|
this.selectedFile.set(this.tabs()[this.selectedTab()]);
|
||||||
|
} else if (this.tabs().length > 1) {
|
||||||
|
this.selectedTab.update((tab) => tab + 1);
|
||||||
|
this.selectedFile.set(this.tabs()[this.selectedTab()]);
|
||||||
|
} else {
|
||||||
|
this.selectedFile.set(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Drop files over viewport
|
||||||
|
protected fileDropped(event: DragEvent) {
|
||||||
|
if (event.dataTransfer?.items.length ?? 0 > 1) {
|
||||||
|
// Open in the tabs
|
||||||
|
} else if (event.dataTransfer?.items.length ?? 0 > 0) {
|
||||||
|
// Open in current tab
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ApplicationConfig,
|
ApplicationConfig,
|
||||||
|
inject,
|
||||||
|
provideAppInitializer,
|
||||||
provideZonelessChangeDetection,
|
provideZonelessChangeDetection,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
@@ -8,6 +10,7 @@ import { providePrimeNG } from 'primeng/config';
|
|||||||
import Aura from '@primeng/themes/aura';
|
import Aura from '@primeng/themes/aura';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
import { DuckdbService } from './duckdb.service';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -19,5 +22,9 @@ export const appConfig: ApplicationConfig = {
|
|||||||
preset: Aura,
|
preset: Aura,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
provideAppInitializer(async () => {
|
||||||
|
const duckDbService = inject(DuckdbService);
|
||||||
|
await duckDbService.init();
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,9 +102,6 @@ const suffix = (operator: FilterOperator) => {
|
|||||||
export class DuckdbService {
|
export class DuckdbService {
|
||||||
private db!: duckdb.AsyncDuckDB;
|
private db!: duckdb.AsyncDuckDB;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
async init() {
|
async init() {
|
||||||
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
|
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
computed,
|
||||||
effect,
|
effect,
|
||||||
inject,
|
inject,
|
||||||
input,
|
input,
|
||||||
@@ -123,34 +124,36 @@ import { PaginatorModule, PaginatorState } from 'primeng/paginator';
|
|||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #footer>
|
@if (hasAggregates()) {
|
||||||
<tr>
|
<ng-template #footer>
|
||||||
@for (col of columns(); track $index) {
|
<tr>
|
||||||
<td #attachment>
|
@for (col of columns(); track $index) {
|
||||||
@if (col.type === 'DOUBLE' || col.type === 'BIGINT') {
|
<td #attachment>
|
||||||
<div class="flex items-baseline">
|
@if (col.type === 'DOUBLE' || col.type === 'BIGINT') {
|
||||||
<p-button
|
<div class="flex items-baseline">
|
||||||
(click)="
|
<p-button
|
||||||
currentAggregateColumn.set($index);
|
(click)="
|
||||||
aggregateMenu.toggle($event)
|
currentAggregateColumn.set($index);
|
||||||
"
|
aggregateMenu.toggle($event)
|
||||||
[label]="aggregates()[$index]?.type?.toUpperCase()"
|
"
|
||||||
>
|
[label]="aggregates()[$index]?.type?.toUpperCase()"
|
||||||
<ng-template #icon>
|
>
|
||||||
<span class="material-symbols-outlined">
|
<ng-template #icon>
|
||||||
mediation
|
<span class="material-symbols-outlined">
|
||||||
</span>
|
mediation
|
||||||
</ng-template>
|
</span>
|
||||||
</p-button>
|
</ng-template>
|
||||||
<span class="flex-1 text-end">
|
</p-button>
|
||||||
{{ aggregateValues()[$index] | number }}
|
<span class="flex-1 text-end">
|
||||||
</span>
|
{{ aggregateValues()[$index] | number }}
|
||||||
</div>
|
</span>
|
||||||
}
|
</div>
|
||||||
</td>
|
}
|
||||||
}
|
</td>
|
||||||
</tr>
|
}
|
||||||
</ng-template>
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
}
|
||||||
</p-table>
|
</p-table>
|
||||||
</div>
|
</div>
|
||||||
@if (currentRowCount() > PAGE_SIZE) {
|
@if (currentRowCount() > PAGE_SIZE) {
|
||||||
@@ -191,6 +194,10 @@ export class FileViewerComponent {
|
|||||||
|
|
||||||
private table = viewChild(Table);
|
private table = viewChild(Table);
|
||||||
|
|
||||||
|
protected hasAggregates = computed(
|
||||||
|
() => !!this.columns().find((col) => this.isAggregateColumn(col)),
|
||||||
|
);
|
||||||
|
|
||||||
protected aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({
|
protected aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({
|
||||||
label: type,
|
label: type,
|
||||||
command: () => this.updateAggregateValue(type),
|
command: () => this.updateAggregateValue(type),
|
||||||
@@ -198,7 +205,7 @@ export class FileViewerComponent {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(async () => {
|
effect(async () => {
|
||||||
this.loadEmpty();
|
await this.loadEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +275,7 @@ export class FileViewerComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Can't update the current value, otherwise we get an infinite loop due to primeng change detection rerunning (plus it's faster to mutate)
|
// 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++) {
|
for (let i = 0; i < rows.rows!.length; i++) {
|
||||||
this.currentValue()[event.first! + i] = rows.rows![i];
|
this.currentValue()[event.first! + i] = rows.rows![i];
|
||||||
}
|
}
|
||||||
event.forceUpdate!();
|
event.forceUpdate!();
|
||||||
@@ -340,4 +347,8 @@ export class FileViewerComponent {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isAggregateColumn(col: Column) {
|
||||||
|
return col.type === 'DOUBLE' || col.type === 'BIGINT';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=mediation"
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=close,mediation"
|
||||||
/>
|
/>
|
||||||
<style>
|
<style>
|
||||||
.material-symbols-outlined {
|
.material-symbols-outlined {
|
||||||
|
|||||||
Reference in New Issue
Block a user