adding data browser

This commit is contained in:
2024-09-08 22:11:51 -04:00
parent e88499cbaf
commit 224e6388c1
48 changed files with 1037 additions and 75 deletions

View File

@@ -3,5 +3,6 @@ import { ChartAxis } from './chart-type';
export interface ChartConfig {
type: ChartType;
title?: string;
axis: ChartAxis[];
}

View File

@@ -32,6 +32,7 @@ export interface IDataset {
xAxisKey?: string;
yAxisKey?: string;
};
spanGaps?: boolean;
stack?: string;
}

View File

@@ -1 +1 @@
<canvas #chart></canvas>
<canvas #chart class="fit"></canvas>

View File

@@ -0,0 +1,4 @@
.fit {
height: 100%;
width: 100%;
}

View File

@@ -46,6 +46,7 @@ export class DatasetService {
if (axis.fill) {
entry.fill = axis.fill;
}
entry.spanGaps = true;
result.push(entry);
});

View File

@@ -15,11 +15,18 @@ export class OptionsService {
result.maintainAspectRatio = false;
result.scales = this.scaleService.scales(chart, data);
if (chart.title) {
result.plugins = result.plugins ?? {};
result.plugins.title = result.plugins.title ?? {};
result.plugins.title.text = chart.title;
}
const fill =
(chart?.axis ?? []).filter((axis: ChartAxis) => axis.fill).length > 0;
if (fill) {
result.plugins = result.plugins ?? {};
result.plugins.filler = { propagate: true };
result.plugins.filler = result.plugins.filler ?? {};
result.plugins.filler.propagate = true;
}
return result;

View File

@@ -0,0 +1 @@
<p>context-menu works!</p>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContextMenuComponent } from './context-menu.component';
describe('ContextMenuComponent', () => {
let component: ContextMenuComponent;
let fixture: ComponentFixture<ContextMenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContextMenuComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ContextMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'fbi-context-menu',
standalone: true,
imports: [],
templateUrl: './context-menu.component.html',
styleUrl: './context-menu.component.scss'
})
export class ContextMenuComponent {
}

View File

@@ -0,0 +1,6 @@
<div class="header">Fields!</div>
<fbi-tree
class="tree"
[node]="node"
(actionClick)="onActionClick($event)"
></fbi-tree>

View File

@@ -0,0 +1,17 @@
:host {
display: flex;
flex-direction: column;
}
.header {
position: sticky;
top: 0px;
border: 1px solid black;
padding-left: 4px;
background-color: lightgray;
}
.tree {
flex: 1;
overflow: scroll;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MetadataComponent } from './metadata.component';
describe('MetadataComponent', () => {
let component: MetadataComponent;
let fixture: ComponentFixture<MetadataComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MetadataComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MetadataComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,75 @@
import { Component } from '@angular/core';
import { TreeComponent } from '../tree/tree.component';
import { TreeNode } from '../../models/tree-node';
import { MetaService } from '../../services/meta.service';
import { Action } from '../../models/action';
import { faAdd } from '@fortawesome/free-solid-svg-icons';
import { QueryService } from '../../services/query.service';
import { combineLatest } from 'rxjs';
import { Query } from '../../models/query';
@Component({
selector: 'fbi-metadata',
standalone: true,
imports: [TreeComponent],
templateUrl: './metadata.component.html',
styleUrl: './metadata.component.scss',
})
export class MetadataComponent {
node: TreeNode = new TreeNode({});
constructor(metaService: MetaService, private queryService: QueryService) {
combineLatest({
meta: metaService.Data,
query: queryService.Query,
}).subscribe((d: { meta: Partial<TreeNode>[]; query: Query }) => {
const inuse = d.query.fields;
const expanded = this.getExpanded(this.node);
const recurse = (node: Partial<TreeNode>) => {
node.hidden = inuse.includes(node.data);
node.expanded = expanded.includes(node.data);
const children = node.children ?? [];
children.forEach((child: Partial<TreeNode>) => recurse(child));
if (children.length === 0) {
const actions = [{ label: 'Add', icon: faAdd, data: ACTIONS.ADD }];
node.actions = actions as Action[];
}
};
(d.meta ?? []).forEach((node: Partial<TreeNode>) => recurse(node));
this.node = new TreeNode({
hidden: true,
expanded: true,
children: d.meta as TreeNode[],
});
});
}
onActionClick(event: { action: Action; node: TreeNode }): void {
switch (event.action.data) {
case ACTIONS.ADD:
this.queryService.add(event.node.data);
break;
}
}
private getExpanded(node: TreeNode): string[] {
const result: string[] = [];
if (node.expanded) {
result.push(node.data);
}
(node.children ?? []).forEach((child: TreeNode) =>
result.push(...this.getExpanded(child))
);
return result;
}
}
enum ACTIONS {
ADD = 'add',
}

View File

@@ -0,0 +1,6 @@
<div class="header">Query!</div>
<fbi-tree
class="tree"
[node]="node"
(actionClick)="onActionClick($event)"
></fbi-tree>

View File

@@ -0,0 +1,17 @@
:host {
display: flex;
flex-direction: column;
}
.header {
position: sticky;
top: 0px;
border: 1px solid black;
padding-left: 4px;
background-color: lightgray;
}
.tree {
flex: 1;
overflow: scroll;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QueryComponent } from './query.component';
describe('QueryComponent', () => {
let component: QueryComponent;
let fixture: ComponentFixture<QueryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [QueryComponent]
})
.compileComponents();
fixture = TestBed.createComponent(QueryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,106 @@
import { Component } from '@angular/core';
import { TreeComponent } from '../tree/tree.component';
import { TreeNode } from '../../models/tree-node';
import { QueryService } from '../../services/query.service';
import { Query } from '../../models/query';
import { Action } from '../../models/action';
import {
faArrowDown,
faArrowUp,
faRemove,
} from '@fortawesome/free-solid-svg-icons';
import { ExecuteService } from '../../services/execute.service';
import { Header } from '../../models/header';
@Component({
selector: 'fbi-query',
standalone: true,
imports: [TreeComponent],
templateUrl: './query.component.html',
styleUrl: './query.component.scss',
})
export class QueryComponent {
node: TreeNode = new TreeNode({});
constructor(
private queryService: QueryService,
executeService: ExecuteService
) {
queryService.Query.subscribe((query: Query) => {
executeService.headers(query).subscribe((headers: Header[]) => {
this.node = new TreeNode({
hidden: true,
expanded: true,
children: [
{
label: 'Fields',
leaf: false,
expanded: true,
children: (query?.fields ?? []).map(
(field: string, index: number, array: string[]) => {
const actions = [];
if (index !== 0) {
actions.push({
label: 'Move Up',
icon: faArrowUp,
data: { cmd: ACTIONS.UP, data: index - 1 },
});
}
if (index !== array.length - 1) {
actions.push({
label: 'Move Down',
icon: faArrowDown,
data: { cmd: ACTIONS.DOWN, data: index + 1 },
});
}
actions.push({
label: 'Remove',
icon: faRemove,
data: { cmd: ACTIONS.REMOVE },
});
const label =
headers.find((header: Header) => header.source === field)
?.label ?? 'unknown';
return {
label: label,
data: field,
actions: actions,
};
}
),
},
{ label: 'Filters', expanded: true, leaf: false },
] as TreeNode[],
});
});
});
}
onActionClick(event: { action: Action; node: TreeNode }): void {
const data = event.action.data as ActionData;
switch (data.cmd) {
case ACTIONS.REMOVE:
this.queryService.remove(event.node.data);
break;
case ACTIONS.UP:
this.queryService.add(event.node.data, data.data);
break;
case ACTIONS.DOWN:
this.queryService.add(event.node.data, data.data);
break;
}
}
}
enum ACTIONS {
REMOVE = 'del',
UP = 'up',
DOWN = 'down',
}
type ActionData = {
cmd: ACTIONS;
data: any;
};

View File

@@ -0,0 +1 @@
<fbi-table [headers]="headers" [rows]="rows"></fbi-table>

View File

@@ -0,0 +1,3 @@
:host {
padding: 8px;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ResultComponent } from './result.component';
describe('ResultComponent', () => {
let component: ResultComponent;
let fixture: ComponentFixture<ResultComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ResultComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ResultComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,49 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { TreeComponent } from '../tree/tree.component';
import { QueryService } from '../../services/query.service';
import { Query } from '../../models/query';
import { ExecuteService } from '../../services/execute.service';
import { forkJoin } from 'rxjs';
import { Header } from '../../models/header';
import { TableComponent } from '../table/table.component';
@Component({
selector: 'fbi-result',
standalone: true,
imports: [CommonModule, TreeComponent, TableComponent],
templateUrl: './result.component.html',
styleUrl: './result.component.scss',
})
export class ResultComponent {
headers: Header[] = [];
rows: Record<string, any>[] = [];
constructor(
queryService: QueryService,
private executeService: ExecuteService
) {
let last: string = '';
queryService.Query.subscribe((query: Query) => {
if (query.isValid()) {
const current = query.toString();
if (last !== current) {
this.load(query);
last = current;
}
}
});
}
private load(query: Query): void {
forkJoin({
headers: this.executeService.headers(query),
data: this.executeService.data(query),
}).subscribe(
(result: { headers: Header[]; data: Record<string, any>[] }) => {
this.headers = result.headers;
this.rows = result.data;
}
);
}
}

View File

@@ -1,9 +0,0 @@
export class Header {
label: string;
source: string;
constructor(data: Partial<Header>) {
this.label = data?.label ?? '';
this.source = data?.source ?? '';
}
}

View File

@@ -1,12 +1,13 @@
<table>
<tr>
<th *ngFor="let h of header">
<th></th>
<th *ngFor="let h of headers">
{{ h.label }}
</th>
</tr>
<tr *ngFor="let row of rows | async; let index">
<td>{{ index }}</td>
<td *ngFor="let h of header">
<tr *ngFor="let row of rows; index as i">
<td>{{ i }}</td>
<td *ngFor="let h of headers">
{{ row[h.source] }}
</td>
</tr>

View File

@@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Header } from './header';
import { Observable } from 'rxjs';
import { Header } from '../../models/header';
@Component({
selector: 'fbi-table',
@@ -11,6 +10,6 @@ import { Observable } from 'rxjs';
styleUrl: './table.component.scss',
})
export class TableComponent {
@Input() header!: Header[];
@Input() rows!: Observable<Record<string, any>[]>;
@Input() headers!: Header[];
@Input() rows!: Record<string, any>[];
}

View File

@@ -0,0 +1,32 @@
<div
*ngIf="!node?.hidden"
class="treenode"
[class.disabled]="node.disabled"
[class.clickable]="!node.leaf"
(click)="toggle($event)"
>
<span *ngIf="!node.leaf" class="icon">
<fa-icon *ngIf="node.expanded" [icon]="faCaretDown"></fa-icon>
<fa-icon *ngIf="!node.expanded" [icon]="faCaretRight"></fa-icon>
</span>
<span [title]="node.label" class="fill ellipsis">{{ node.label }}</span>
<ng-container *ngIf="node.leaf && !node.disabled" class="actions">
<fa-icon
*ngFor="let action of node.actions"
class="action clickable"
[icon]="action.icon"
(click)="onActionClick($event, action)"
></fa-icon>
</ng-container>
</div>
<div
*ngIf="node && node.children && node.expanded"
[class.branch]="node.label !== ''"
>
<fbi-tree
*ngFor="let child of node.children"
[node]="child"
(actionClick)="passActionClick($event)"
></fbi-tree>
</div>

View File

@@ -0,0 +1,56 @@
.branch {
margin-left: 20px;
overflow: hidden;
}
.treenode {
color: #333;
margin: 0;
white-space: nowrap;
display: flex;
margin-right: 2px;
}
.treenode:hover {
background-color: #edf8fa;
}
.icon {
display: inline-block;
width: 16px;
text-align: center;
}
.fill {
flex: 1;
}
.clickable {
cursor: pointer;
}
.disabled {
color: dimgray;
opacity: 0.5;
}
.right {
float: right;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.treenode:hover > .action {
visibility: visible;
width: auto;
}
.action {
visibility: hidden;
width: 0px;
padding-left: 2px;
}

View File

@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TreeComponent } from './tree.component';
describe('TreeComponent', () => {
let component: TreeComponent;
let fixture: ComponentFixture<TreeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TreeComponent],
}).compileComponents();
fixture = TestBed.createComponent(TreeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,40 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TreeNode } from '../../models/tree-node';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { Action } from '../../models/action';
@Component({
selector: 'fbi-tree',
standalone: true,
imports: [CommonModule, FontAwesomeModule],
templateUrl: './tree.component.html',
styleUrl: './tree.component.scss',
})
export class TreeComponent {
@Input() node!: TreeNode;
@Output() actionClick = new EventEmitter<{
action: Action;
node: TreeNode;
}>();
faCaretDown = faCaretDown;
faCaretRight = faCaretRight;
toggle(event: MouseEvent): void {
if (!this.node.leaf) {
event.stopPropagation();
this.node.expanded = !this.node.expanded;
}
}
passActionClick(event: { action: Action; node: TreeNode }): void {
this.actionClick.emit(event);
}
onActionClick(event: MouseEvent, action: Action): void {
event.stopPropagation();
this.actionClick.emit({ action: action, node: this.node });
}
}