Compare commits

...

10 Commits

Author SHA1 Message Date
1ca4f18e3d a somewhat working example of a dashboard 2023-03-12 17:33:26 +01:00
add30a5ee9 use a drag handle with icon 2023-03-12 17:02:40 +01:00
e42c00cc08 update libraries 2023-03-12 11:43:17 +01:00
c9758adbef increase socket timeout
The old value of 2ms lead to 45% load for a single core
2023-03-12 11:04:27 +01:00
dafe6813ed enable save button only if dashboard is dirty 2023-03-12 09:36:33 +01:00
bc0ae23de5 delete plot from dashboard 2023-03-12 09:15:56 +01:00
02c748c286 delete text from dashboard 2023-03-12 09:11:49 +01:00
44aed2883d edit name and description on dashboard page 2023-03-12 08:52:54 +01:00
96ed788793 cleanup 2023-03-12 08:32:42 +01:00
b5028e03be edit plots 2023-03-12 08:24:16 +01:00
33 changed files with 568 additions and 301 deletions

View File

@@ -4,7 +4,7 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'java'
id 'eclipse'
id 'com.github.ben-manes.versions' version "0.45.0" // check for dependency updates run: gradlew dependenyUpdates
id 'com.github.ben-manes.versions' version "0.46.0" // check for dependency updates run: gradlew dependenyUpdates
}
@@ -13,7 +13,7 @@ ext {
javaVersion=17
version_log4j2= '2.19.0' // keep in sync with spring-boot-starter-log4j2
version_spring = '3.0.2'
version_spring = '3.0.4'
version_junit = '5.9.2'
version_junit_platform = '1.9.2'
version_nodejs = '16.17.1' // keep in sync with npm
@@ -136,5 +136,5 @@ subprojects {
}
wrapper {
gradleVersion = '8.0.1'
gradleVersion = '8.0.2'
}

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
liquibase.properties Normal file
View File

@@ -0,0 +1,2 @@
liquibase.hub.mode=off

2
pdb-js/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -33,6 +33,8 @@ import { DashboardPageComponent } from './dashboard-page/dashboard-page.componen
import { NewDashboardComponent } from './dashboard-page/new-dashboard/new-dashboard.component';
import { MatDialogModule, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
import {MatTableModule} from '@angular/material/table';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatCardModule} from '@angular/material/card';
import {MatBadgeModule} from '@angular/material/badge';
import { DashboardComponent } from './dashboard-page/dashboard/dashboard.component';
import { AddTextDialogComponent } from './dashboard-page/dashboard/add-text-dialog/add-text-dialog.component';
@@ -83,9 +85,11 @@ import { FocusDirective } from './focus.directive';
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatGridListModule,
MatInputModule,
MatRadioModule,
MatProgressBarModule,

View File

@@ -1,121 +1,110 @@
<style>
.customizable-grid {
width: 100%;
display: flex;
flex-wrap: wrap;
}
.draggable {
flex-shrink: 0;
padding: 0.5rem;
border: solid 1px;
border-collapse: collapse;
}
.draggable-small {
width: 300px;
height: 300px;
background-color: rgb(158, 240, 189);
}
.draggable-middle {
width: 600px;
height: 300px;
background-color: darksalmon;
}
.example-container {
width: 400px;
max-width: 100%;
margin: 0 25px 25px 0;
display: inline-block;
vertical-align: top;
.example-list {
list-style-type: none;
padding: 0;
}
.example-list {
border: solid 1px #ccc;
min-height: 60px;
background: white;
border-radius: 4px;
overflow: hidden;
display: block;
.example-list li {
display: table-cell;
padding: 4px;
}
.example-container {
display: flex;
flex-wrap: wrap;
min-width: 600px;
max-width: 1200px;
}
.example-box {
padding: 20px 10px;
border-bottom: solid 1px #ccc;
width: var(--box-width);
height: var(--box-height);
border: solid 1px #ccc;
font-size: 30pt;
font-weight: bold;
color: rgba(0, 0, 0, 0.87);
cursor: grab;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
cursor: move;
background: white;
font-size: 14px;
text-align: center;
background: #fff;
border-radius: 4px;
position: relative;
z-index: 1;
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.example-box--wide {
width: 400px;
height: var(--box-height);
}
.example-box:active {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
opacity: 0.6;
}
.cdk-drop-list {
display: flex;
padding-right: 10px;
padding-bottom: 10px;
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
opacity: 0.3;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.example-box:last-child {
border: none;
.cdk-drop-list-dragging {
cursor: grabbing !important;
}
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
button {
margin-right: 4px;
}
</style>
<!--
<div class="customizable-grid">
<div class="draggable draggable-small"></div>
<div class="draggable draggable-middle"></div>
<div class="draggable draggable-small"></div>
<div class="draggable draggable-middle"></div>
</div>
-->
<h1>Drag&Drop with a flex-wrap</h1>
<div class="example-container">
<h2>To do</h2>
<button (click)="add()">Add</button>
<button (click)="shuffle()">Shuffle</button>
<br />
<ul class="example-list">
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<div
class="example-container"
cdkDropListGroup
[ngStyle]="{ '--box-width': boxWidth, '--box-height': boxHeight }"
>
<div
cdkDropList
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList]"
class="example-list"
(cdkDropListDropped)="drop($event)">
<div class="example-box" *ngFor="let item of todo" cdkDrag>{{item}}</div>
</div>
</div>
<div class="example-container">
<h2>Done</h2>
(cdkDropListEntered)="onDropListEntered($event)"
(cdkDropListDropped)="onDropListDropped()"
></div>
<div
cdkDropList
#doneList="cdkDropList"
[cdkDropListData]="done"
[cdkDropListConnectedTo]="[todoList]"
class="example-list"
(cdkDropListDropped)="drop($event)">
<div class="example-box" *ngFor="let item of done" cdkDrag>{{item}}</div>
(cdkDropListEntered)="onDropListEntered($event)"
(cdkDropListDropped)="onDropListDropped()"
*ngFor="let item of items"
>
<div cdkDrag class="example-box" [ngClass]="{'example-box--wide': item%2==1}">{{ item }}</div>
</div>
</div>

View File

@@ -1,24 +1,119 @@
import { Component } from '@angular/core';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';
import { CdkDragEnter, CdkDropList, moveItemInArray, DragRef} from '@angular/cdk/drag-drop';
import { AfterViewInit } from '@angular/core';
import { ViewChild } from '@angular/core';
@Component({
selector: 'app-customizable-grid',
templateUrl: './customizable-grid.component.html'
})
export class CustomizableGridComponent {
todo = ['Get to work', 'Pick up groceries', 'Go home', 'Fall asleep'];
done = ['Get up', 'Brush teeth', 'Take a shower', 'Check e-mail', 'Walk dog'];
export class CustomizableGridComponent implements AfterViewInit {
@ViewChild(CdkDropList) placeholder!: CdkDropList;
drop(event: CdkDragDrop<string[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex,
private target: CdkDropList|null = null;
private targetIndex: number = 0;
private source: CdkDropList|null = null;
private sourceIndex: number = 0;
private dragRef: DragRef|null = null;
items: Array<number> = [1, 2, 3, 4, 5, 6, 7, 8, 9];
boxWidth = '200px';
boxHeight = '200px';
ngAfterViewInit() {
const placeholderElement = this.placeholder.element.nativeElement;
placeholderElement.style.display = 'none';
placeholderElement.parentNode!.removeChild(placeholderElement);
}
add() {
this.items.push(this.items.length + 1);
}
shuffle() {
this.items.sort(function () {
return 0.5 - Math.random();
});
}
onDropListDropped() {
if (!this.target) {
return;
}
const placeholderElement: HTMLElement =
this.placeholder.element.nativeElement;
const placeholderParentElement: HTMLElement =
placeholderElement.parentElement!;
placeholderElement.style.display = 'none';
placeholderParentElement.removeChild(placeholderElement);
placeholderParentElement.appendChild(placeholderElement);
placeholderParentElement.insertBefore(
this.source!.element.nativeElement,
placeholderParentElement.children[this.sourceIndex]
);
if (this.placeholder._dropListRef.isDragging() && this.dragRef != null) {
this.placeholder._dropListRef.exit(this.dragRef);
}
this.target = null;
this.source = null;
this.dragRef = null;
if (this.sourceIndex !== this.targetIndex) {
moveItemInArray(this.items, this.sourceIndex, this.targetIndex);
}
}
onDropListEntered({ item, container }: CdkDragEnter) {
if (container == this.placeholder) {
return;
}
const placeholderElement: HTMLElement =
this.placeholder.element.nativeElement;
const sourceElement: HTMLElement = item.dropContainer.element.nativeElement;
const dropElement: HTMLElement = container.element.nativeElement;
const dragIndex: number = Array.prototype.indexOf.call(
dropElement.parentElement!.children,
this.source ? placeholderElement : sourceElement
);
const dropIndex: number = Array.prototype.indexOf.call(
dropElement.parentElement!.children,
dropElement
);
if (!this.source) {
this.sourceIndex = dragIndex;
this.source = item.dropContainer;
placeholderElement.style.width = this.boxWidth + 'px';
placeholderElement.style.height = this.boxHeight + 40 + 'px';
sourceElement.parentElement!.removeChild(sourceElement);
}
this.targetIndex = dropIndex;
this.target = container;
this.dragRef = item._dragRef;
placeholderElement.style.display = '';
dropElement.parentElement!.insertBefore(
placeholderElement,
dropIndex > dragIndex ? dropElement.nextSibling : dropElement
);
this.placeholder._dropListRef.enter(
item._dragRef,
item.element.nativeElement.offsetLeft,
item.element.nativeElement.offsetTop
);
}
}
}

View File

@@ -1,14 +1,8 @@
<style>
:host {
/*
height: calc(100% - 29px);
*/
width: 100%;
position: absolute;
padding: 0.5em;
}
.center {
position: absolute;
top: 50%;
@@ -23,12 +17,15 @@
font-weight: bold;
color: #333;
}
.no-break {
white-space: nowrap;
}
</style>
<div *ngIf="loading" class="center">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="error" class="center is-error center-content ">
<div *ngIf="error" class="center is-error center-content">
{{error}}
</div>
<div *ngIf="!loading && !error">
@@ -55,11 +52,14 @@
<th mat-header-cell *matHeaderCellDef>Description</th>
<td mat-cell *matCellDef="let element">{{element.description}}</td>
</ng-container>
<!-- Delete Column -->
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button (click)="delete(element)">
<!-- actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let element" class="no-break">
<button mat-icon-button (click)="edit(element)" aria-label="edit dashboard" title="edit dashboard">
<img src="assets/img/edit-outline.svg" class="icon-small" title="delete" />
</button>
<button mat-icon-button (click)="delete(element)" aria-label="delete dashboard" title="delete dashboard">
<img src="assets/img/recycle-bin-line.svg" class="icon-small" title="delete" />
</button>
</td>

View File

@@ -1,6 +1,7 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { Dashboard, DashboardCreationData, DashboardList, DashboardService } from '../dashboard.service';
import { NewDashboardComponent } from './new-dashboard/new-dashboard.component';
@@ -11,14 +12,15 @@ import { NewDashboardComponent } from './new-dashboard/new-dashboard.component';
})
export class DashboardPageComponent implements OnInit {
displayedColumns: string[] = [/*'icon',*/ 'name', 'description','delete'];
displayedColumns: string[] = [/*'icon',*/ 'name', 'description','actions'];
dataSource: Dashboard[] = [];
loading = true;
error = "";
constructor(public dialog: MatDialog, private dashboardService: DashboardService){
}
constructor(
public dialog: MatDialog,
private dashboardService: DashboardService,
private snackBar: MatSnackBar){}
ngOnInit(): void {
this.refreshTable();
@@ -44,39 +46,57 @@ export class DashboardPageComponent implements OnInit {
createNewDashboard() {
const dialogRef = this.dialog.open(NewDashboardComponent, {
data: {name: "", description: ""},
hasBackdrop: true
data: {name: "", description: ""}
});
dialogRef.afterClosed().subscribe((result: DashboardCreationData) => {
console.log('The dialog was closed with result ', JSON.stringify(result));
this.dashboardService.createDashboard(result).subscribe(result => {
console.log(result);
this.refreshTable();
this.dashboardService.createDashboard(result).subscribe(result => this.refreshTable());
});
}
edit(dashboard: Dashboard) {
const dialogRef = this.dialog.open(NewDashboardComponent, {
data: {name: dashboard.name, description: dashboard.description},
hasBackdrop: true
});
dialogRef.afterClosed().subscribe((result?: DashboardCreationData) => {
if (result) {
dashboard.name = result.name;
dashboard.description = result.description;
this.dashboardService.saveDashboard(dashboard).subscribe({
'error': () => {
this.snackBar.open("server made a boo boo", "", {
duration:5000
})
},
complete: () => {
this.refreshTable()
}
});
}
});
}
delete(dashboard: Dashboard){
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {title: "", text: "Delete dashboard '"+dashboard.name+"'?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"},
hasBackdrop: true
data: {title: "", text: "Delete dashboard '"+dashboard.name+"'?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
});
dialogRef.afterClosed().subscribe((result: boolean) => {
if (result === true) {
this.dashboardService.deleteDashboard(dashboard.id).subscribe({
'error': (error) => {
this.snackBar.open("failed to delete dashboard","", {
duration: 5000,
verticalPosition: 'top'
});
},
'complete': () => this.refreshTable()
});
}
});
}
}

View File

@@ -14,11 +14,11 @@
max-height: unset;
}
</style>
<h1 mat-dialog-title>Add Plot</h1>
<h1 mat-dialog-title>{{data.title}}</h1>
<pdb-visualization-page mat-dialog-content #plot></pdb-visualization-page>
<pdb-visualization-page mat-dialog-content #plot [defaultConfig]="data.config" [galleryEnabled]="false"></pdb-visualization-page>
<div mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button mat-dialog-close >Cancel</button>
<button class="save-button" mat-button mat-dialog-close (click)="onSaveClick()">Save</button>
</div>

View File

@@ -1,17 +1,22 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { AfterViewInit, Component, ElementRef, Inject, ViewChild } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { PlotConfig } from 'src/app/plot.service';
import { VisualizationPageComponent } from 'src/app/visualization-page/visualization-page.component';
@Component({
selector: 'app-add-plot-dialog',
templateUrl: './add-plot-dialog.component.html',
styleUrls: ['./add-plot-dialog.component.scss']
templateUrl: './add-plot-dialog.component.html'
})
export class AddPlotDialogComponent {
@ViewChild("plot") plotElement! :VisualizationPageComponent;
constructor(public dialogRef: MatDialogRef<string>){
constructor(
public dialogRef: MatDialogRef<PlotConfig | undefined>,
@Inject(MAT_DIALOG_DATA) public data: {config: PlotConfig, title: string}
){
}
onSaveClick(): void {

View File

@@ -18,7 +18,6 @@ export class AddTextDialogComponent {
this.dialogRef.close(undefined);
}
onSaveClick(): void {
this.dialogRef.close(this.text);
}

View File

@@ -2,7 +2,6 @@
:host {
width: 100%;
height: 100%;
}
.center {
position: absolute;
@@ -26,19 +25,19 @@
display: flex;
flex-direction: column;
}
.editable .editable-hovered {
visibility: hidden;
}
.editable:hover .editable-hovered {
visibility: visible;
}
.handle {
display: block;
height: 1.5em;
visibility: hidden;
width: fit-content;
}
[cdkDrag] {
position: relative;
}
[cdkDrag]:hover .handle, .cdk-drop-list-dragging .handle {
background-color: #f3f3f3;
border-radius: 3px;
cursor: grab;
visibility: visible;
}
</style>
@@ -53,7 +52,7 @@
<div class="toolbar">
<button mat-button (click)="addText()">Add Text</button>
<button mat-button (click)="addPlot()">Add Plot</button>
<button class="save-button" mat-button (click)="save()">Save</button>
<button class="save-button" mat-button (click)="save()" [disabled]="!isDirty()">Save</button>
</div>
<div class="editable">
<h1>{{dashboard.name}}<button mat-icon-button (click)="editNameAndDescription()" class="editable-hovered"><img src="/assets/img/edit-outline.svg"/></button></h1>
@@ -61,7 +60,6 @@
</div>
<div cdkDropListGroup>
<!-- All lists in here will be connected. -->
<div
cdkDropList
class="dashboard-column"
@@ -72,13 +70,13 @@
cdkDrag
*ngFor="let id of column"
[attr.widget-id]="id">
<div cdkDragHandle class="handle"></div>
<div cdkDragHandle class="handle"><img src="/assets/img/drag_handle.svg" class="icon-small"/></div>
<app-text-widget
*ngIf="isTextWidget(id)"
[data]="getTextWidget(id)!"></app-text-widget>
[data]="getTextWidget(id)!" (deleted)="delete($event)"></app-text-widget>
<app-plot-widget
*ngIf="isPlotWidget(id)"
[data]="getPlotWidget(id)!"></app-plot-widget>
[data]="getPlotWidget(id)!" (deleted)="delete($event)"></app-plot-widget>
</div>
</div>
</div>

View File

@@ -4,8 +4,8 @@ import { Component, ElementRef, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute } from '@angular/router';
import { BaseWidget, Dashboard, DashboardCreationData, DashboardService, PlotSize, PlotWidget, PlotWidgetRenderData, TextWidget } from 'src/app/dashboard.service';
import { PlotConfig, PlotRequest, PlotResponse, PlotService, RenderOptions } from 'src/app/plot.service';
import { Dashboard, DashboardCreationData, DashboardService, PlotWidget, PlotWidgetRenderData, TextWidget } from 'src/app/dashboard.service';
import { PlotConfig, PlotResponse, PlotService } from 'src/app/plot.service';
import { NewDashboardComponent } from '../new-dashboard/new-dashboard.component';
import { AddPlotDialogComponent } from './add-plot-dialog/add-plot-dialog.component';
import { AddTextDialogComponent } from './add-text-dialog/add-text-dialog.component';
@@ -18,6 +18,8 @@ export class DashboardComponent implements OnInit {
dashboard?: Dashboard = undefined;
pristineDashboardJSON?: string = undefined;
error = "";
plotWidgetRenderData: PlotWidgetRenderData[] = [];
@@ -34,6 +36,7 @@ export class DashboardComponent implements OnInit {
this.service.getDashboard(<string>this.route.snapshot.paramMap.get("id")).subscribe({
'next':(dashboard: Dashboard) => {
this.dashboard = dashboard;
this.pristineDashboardJSON = JSON.stringify(dashboard);
this.repairArrangement();
dashboard.plots.forEach(p => {
@@ -54,11 +57,15 @@ export class DashboardComponent implements OnInit {
});
}
isDirty() {
return this.pristineDashboardJSON !== JSON.stringify(this.dashboard);
}
loadImages(index: number, plotWidgetQueue: PlotWidgetRenderData[]) {
if (index < plotWidgetQueue.length){
const plot = plotWidgetQueue[index];
const request = this.createPlotRequest(plot.widget)
const request = PlotWidget.createPlotRequest(plot.widget);
this.plotService.sendPlotRequest(request).subscribe({
next: (response: PlotResponse)=> {
plot.plotResponse= response;
@@ -71,47 +78,6 @@ export class DashboardComponent implements OnInit {
}
}
createPlotRequest(plotWidget: PlotWidget): PlotRequest {
const height = this.height(plotWidget.size);
const width = this.width(plotWidget.size);
const fullWidth = window.innerWidth-30;
const fullHeight = window.innerHeight-30;
const request = new PlotRequest(
(<any>window).submitterId+crypto.randomUUID(),
plotWidget.config,
{
'main': new RenderOptions(height,width, false, true),
'fullScreen': new RenderOptions(fullHeight,fullWidth, false, true)
}
);
return request;
}
height(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 300;
case 'MEDIUM':
return 400;
case 'LARGE':
return 600;
}
}
width(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 400;
case 'MEDIUM':
return 600;
case 'LARGE':
return 900;
}
}
private repairArrangement(){
const arrangement = this.dashboard!.arrangement || [];
if (arrangement.length == 0){
@@ -153,6 +119,7 @@ export class DashboardComponent implements OnInit {
addPlot() {
this.dialog.open(AddPlotDialogComponent,{
data: {title: "Add Plot"},
width: 'calc(100% - 1em)',
height: 'calc(100% - 1em)'
}).afterClosed().subscribe((config: PlotConfig | "") => {
@@ -201,6 +168,7 @@ export class DashboardComponent implements OnInit {
duration: 5000,
verticalPosition: 'top'
});
this.pristineDashboardJSON = JSON.stringify(this.dashboard);
}
});
}
@@ -218,7 +186,6 @@ export class DashboardComponent implements OnInit {
this.dashboard!.description = result.description;
}
});
}
isTextWidget(id: string): boolean {
@@ -249,4 +216,10 @@ export class DashboardComponent implements OnInit {
);
}
}
delete(dashboardId: string) {
this.dashboard!.arrangement[0] = this.dashboard!.arrangement[0].filter(a => a != dashboardId);
this.dashboard!.plots = this.dashboard!.plots.filter(p => p.id != dashboardId);
this.dashboard!.texts = this.dashboard!.texts.filter(t => t.id != dashboardId);
}
}

View File

@@ -3,7 +3,7 @@
float: left;
}
.dashboard-card {
border: solid 1px red;
position: relative;
display: flex;
justify-content: center;
align-items: center;
@@ -19,8 +19,31 @@
img {
cursor: zoom-in;
}
.top-left {
position: absolute;
left: 0;
top: 0;
}
.top-right {
position: absolute;
right: 0;
top: 0;
}
.dashboard-card .editable-hovered {
visibility: hidden;
}
.dashboard-card:hover .editable-hovered {
visibility: visible;
}
</style>
<div class="dashboard-card" [ngClass]="{'size-medium' : true}">
<div class="editable-hovered top-right">
<button mat-icon-button (click)="edit()" ><img src="/assets/img/edit-outline.svg"/></button>
<button mat-icon-button (click)="delete()"><img src="/assets/img/recycle-bin-line.svg"/></button>
</div>
<mat-spinner *ngIf="!hasRender('main') && !isError"></mat-spinner>
<img *ngIf="hasRender('main')" [src]="getImageUrl('main')" (click)="showFullScreenImage()" />
<div *ngIf="isError">

View File

@@ -1,15 +1,17 @@
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmationDialogComponent } from 'src/app/confirmation-dialog/confirmation-dialog.component';
import { PlotWidget, PlotWidgetRenderData } from 'src/app/dashboard.service';
import { PlotViewComponent } from 'src/app/plot-view/plot-view.component';
import { PlotRequest, PlotResponse, PlotService, RenderOptions } from 'src/app/plot.service';
import { PlotConfig, PlotResponse, PlotService } from 'src/app/plot.service';
import { AddPlotDialogComponent } from '../add-plot-dialog/add-plot-dialog.component';
import { FullScreenPlotDialogComponent } from '../full-screen-plot-dialog/full-screen-plot-dialog.component';
@Component({
selector: 'app-plot-widget',
templateUrl: './plot-widget.component.html'
})
export class PlotWidgetComponent implements AfterViewInit {
export class PlotWidgetComponent {
@Input("data")
data!: PlotWidgetRenderData;
@@ -19,29 +21,65 @@ export class PlotWidgetComponent implements AfterViewInit {
@ViewChild("plotView") plotView!: PlotViewComponent;
constructor(private dialog: MatDialog, ){}
@Output()
deleted : EventEmitter<string> = new EventEmitter<string>();
ngAfterViewInit(): void {
constructor(private dialog: MatDialog, private service: PlotService){}
}
hasRender(name: string): boolean{
const hasRender = this.data !== undefined && this.data.plotResponse !== undefined && this.data.plotResponse?.rendered[name] !== undefined;
return hasRender;
return this.data !== undefined && this.data.plotResponse !== undefined && this.data.plotResponse?.rendered[name] !== undefined;
}
getImageUrl(name: string ): string | undefined {
return this.data?.plotResponse?.rendered[name];
}
showFullScreenImage(){
showFullScreenImage() {
this.dialog.open(FullScreenPlotDialogComponent,{
width: 'calc(100% - 15px)',
height: 'calc(100% - 15px)',
'data': {'imageUrl': this.getImageUrl('fullScreen')}
data: {'imageUrl': this.getImageUrl('fullScreen')}
}).afterClosed().subscribe(() => {
});
}
delete() {
this.dialog
.open(ConfirmationDialogComponent, {
data: {title: "", text: "Delete plot?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
})
.afterClosed()
.subscribe((result: boolean) => {
if (result === true) {
this.deleted.emit(this.data.widget.id);
}
});
}
edit() {
this.dialog.open(AddPlotDialogComponent, {
data: {config: this.data.widget.config, title:"Edit Plot"},
width: 'calc(100% - 15px)',
height: 'calc(100% - 15px)',
}).afterClosed().subscribe((config?: PlotConfig) => {
if (config !== undefined && config.query.length > 0) {
this.data.widget.config = config;
this.isError = false;
this.data.plotResponse = undefined;
const request = PlotWidget.createPlotRequest(this.data.widget);
this.service.sendPlotRequest(request).subscribe({
next: (response: PlotResponse)=> {
this.data.plotResponse = response;
},
error: (error:any)=> {
this.isError = true;
},
});
}
});
}
}

View File

@@ -4,7 +4,7 @@
}
.text-widget {
position: relative;
padding: 1em 0;
padding-bottom: 1em;
}
.text-widget:hover {
/*outline: solid 1px black;/**/
@@ -25,6 +25,9 @@
}
</style>
<div class="text-widget">
<button mat-icon-button (click)="edit()" class="editable-hovered"><img src="/assets/img/edit-outline.svg"/></button>
<div class="editable-hovered">
<button mat-icon-button (click)="edit()"><img src="/assets/img/edit-outline.svg"/></button>
<button mat-icon-button (click)="delete()"><img src="/assets/img/recycle-bin-line.svg"/></button>
</div>
<p *ngFor="let line of lines()">{{line}}</p>
</div>

View File

@@ -1,5 +1,6 @@
import { Component, Input } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmationDialogComponent } from 'src/app/confirmation-dialog/confirmation-dialog.component';
import { TextWidget } from 'src/app/dashboard.service';
import { AddTextDialogComponent } from '../add-text-dialog/add-text-dialog.component';
@@ -11,11 +12,26 @@ export class TextWidgetComponent {
@Input()
data! : TextWidget;
@Output()
deleted : EventEmitter<string> = new EventEmitter<string>();
constructor(private dialog: MatDialog){}
lines(): string[]{
return typeof this.data.text == 'string' ? this.data.text.split(/\r?\n/) : [];
}
delete() {
this.dialog
.open(ConfirmationDialogComponent, {
data: {title: "", text: "Delete text?", btnOkLabel: "Delete", btnCancelLabel: "Cancel"}
})
.afterClosed()
.subscribe((result: boolean) => {
if (result === true) {
this.deleted.emit(this.data.id);
}
});
}
edit() {
this.dialog.open(AddTextDialogComponent,{

View File

@@ -1,3 +1,8 @@
<style>
div[mat-dialog-content] {
overflow: hidden;
}
</style>
<h1 mat-dialog-title>Create a new dashboard</h1>
<div mat-dialog-content>
<mat-form-field class="pdb-form-full-width">

View File

@@ -1,4 +0,0 @@
div[mat-dialog-content] {
overflow: hidden;
}

View File

@@ -1,11 +1,10 @@
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import {MatDialog, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import { DashboardCreationData } from 'src/app/dashboard.service';
@Component({
selector: 'app-new-dashboard',
templateUrl: './new-dashboard.component.html',
styleUrls: ['./new-dashboard.component.scss']
templateUrl: './new-dashboard.component.html'
})
export class NewDashboardComponent implements OnInit {

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { PlotConfig, PlotResponse } from './plot.service';
import { PlotConfig, PlotRequest, PlotResponse, RenderOptions } from './plot.service';
@Injectable({
providedIn: 'root'
@@ -67,6 +67,47 @@ export class PlotWidget extends BaseWidget {
constructor(override id: string, override size: 'SMALL'|'MEDIUM'|'LARGE', public config: PlotConfig) {
super(id, 'PLOT', size);
}
public static createPlotRequest(widget: PlotWidget): PlotRequest {
const height = this.height(widget.size);
const width = this.width(widget.size);
const fullWidth = window.innerWidth-30;
const fullHeight = window.innerHeight-30;
const request = new PlotRequest(
(<any>window).submitterId+crypto.randomUUID(),
widget.config,
{
'main': new RenderOptions(height,width, false, true),
'fullScreen': new RenderOptions(fullHeight,fullWidth, false, true)
}
);
return request;
}
static height(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 300;
case 'MEDIUM':
return 400;
case 'LARGE':
return 600;
}
}
static width(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 400;
case 'MEDIUM':
return 600;
case 'LARGE':
return 900;
}
}
}
export type PlotSize = 'SMALL'|'MEDIUM'|'LARGE';

View File

@@ -17,6 +17,7 @@
<mat-form-field class="pdb-form-number"
*ngIf="limitBy !== 'NO_LIMIT'">
<mat-label><!--empty label needed for layout reasons--></mat-label>
<input
matInput
type="number"

View File

@@ -93,20 +93,15 @@ export class PlotService {
export class PlotType {
id: string;
name: string;
icon: string
active: boolean;
xAxis: DataType;
yAxis: DataType;
constructor(id: string, name: string, icon: string, active: boolean, xAxis: DataType, yAxis: DataType) {
this.id = id;
this.name = name;
this.icon = icon;
this.active = active;
this.xAxis = xAxis;
this.yAxis = yAxis;
constructor(
public id: string,
public name: string,
public icon: string,
public active: boolean,
public xAxis: DataType,
public yAxis: DataType) {
}
compatible(others: Array<PlotType>) : boolean {

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Input, ViewChild } from '@angular/core';
import {UntypedFormControl} from '@angular/forms';
import { Component, OnInit, Input, ViewChild, AfterViewInit } from '@angular/core';
import {FormControl, UntypedFormControl} from '@angular/forms';
import {Observable} from 'rxjs';
import {startWith, map} from 'rxjs/operators';
import {MatAutocompleteTrigger } from '@angular/material/autocomplete';
@@ -12,7 +12,7 @@ import { PlotService, PlotType, AutocompleteResult, Suggestion, ResultMode } fro
})
export class QueryAutocompleteComponent implements OnInit {
queryField = new UntypedFormControl('');
queryField = new FormControl<Suggestion>(new Suggestion("","",0));
suggestions = new UntypedFormControl();
@@ -20,26 +20,33 @@ export class QueryAutocompleteComponent implements OnInit {
query : string = "";
suggestionFetcherEnabled = true;
@ViewChild(MatAutocompleteTrigger)
autocomplete!: MatAutocompleteTrigger;
constructor(private plotService: PlotService) {}
constructor(private plotService: PlotService) {
}
ngOnInit() {
const that = this;
this.query = "";
this.queryField.valueChanges.subscribe(function(value){
this.queryField.valueChanges.subscribe((value) =>{
if (value != null) {
if (typeof value == "string") {
that.query = value;
this.query = value;
}else{
that.query = value.newQuery;
this.query = value.newQuery;
var el : HTMLInputElement = <HTMLInputElement>document.getElementById('query-autocomplete-input');
const el : HTMLInputElement = <HTMLInputElement>document.getElementById('query-autocomplete-input');
el.selectionStart=value.newCaretPosition;
el.selectionEnd=value.newCaretPosition;
}
that.fetchSuggestions(value.newCaretPosition);
if (this.suggestionFetcherEnabled) {
this.fetchSuggestions(value.newCaretPosition);
}
}
});
this.filteredSuggestions = this.suggestions.valueChanges.pipe(
@@ -61,23 +68,21 @@ export class QueryAutocompleteComponent implements OnInit {
const that = this;
const query = typeof this.queryField.value == "string"
? this.queryField.value
: this.queryField.value.newQuery;
: this.queryField.value!.newQuery;
this.plotService
.autocomplete(query, caretIndex, ResultMode.CUT_AT_DOT)
.subscribe(
(data: AutocompleteResult) => {// success path
//console.log(JSON.stringify(data.proposals));
.subscribe({
next: (data: AutocompleteResult) => {
that.suggestions.setValue(data.proposals);
that.autocomplete.openPanel();
},
(error:any) => console.log(error)
error: (error:any) => console.log(error)
}
);
}
displaySuggestion(suggestion?: Suggestion): string {
//console.log("suggestion: "+JSON.stringify(suggestion));
return suggestion ? suggestion.newQuery : '';
}
}

View File

@@ -52,7 +52,7 @@
<pdb-y-axis-definition #y1AxisDefinitionComponent yIndex="1"></pdb-y-axis-definition>
<pdb-y-axis-definition #y2AxisDefinitionComponent yIndex="2" [hidden]="!y2AxisAvailable"></pdb-y-axis-definition>
<mat-checkbox [(ngModel)]="enableGallery">Gallery</mat-checkbox>
<mat-checkbox *ngIf="galleryEnabled" [(ngModel)]="enableGallery">Gallery</mat-checkbox>
<mat-form-field *ngIf="enableGallery" class="pdb-form-full-width">
<mat-label>Split By:</mat-label>

View File

@@ -15,11 +15,11 @@
grid:
"query-box query-box date-box" auto
"filters results results" 1fr
/ 25.5em 3fr 24em;
/ 25.5em 3fr 23.5em;
}
}
@media screen and (max-width: 1000px) {
@media screen and (max-width: 900px) {
#visualization {
display: grid;
margin: 0;
@@ -27,7 +27,7 @@
grid:
"query-box" auto
"date-box" auto
"filters" auto
"filters" min-content
"results" 1fr
/ 1fr;
}

View File

@@ -1,13 +1,12 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { PlotService, PlotType, PlotRequest, PlotResponse, TagField, FilterDefaults, DataType, YAxisDefinition, AxesTypes, PlotConfig, RenderOptions, RenderOptionsMap } from '../plot.service';
import { UntypedFormControl, Validators } from '@angular/forms';
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
import { PlotService, PlotType, PlotRequest, TagField, FilterDefaults, DataType, AxesTypes, PlotConfig, RenderOptions, RenderOptionsMap, Suggestion } from '../plot.service';
import { UntypedFormControl, } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { LimitByComponent } from '../limit-by/limit-by.component';
import { YAxisDefinitionComponent } from '../y-axis-definition/y-axis-definition.component';
import { QueryAutocompleteComponent } from '../query-autocomplete/query-autocomplete.component';
import { PlotViewComponent, SelectionRange, DateAnchor, LoadingEvent } from '../plot-view/plot-view.component';
import { PlotViewComponent, LoadingEvent } from '../plot-view/plot-view.component';
import { GalleryViewComponent } from '../gallery-view/gallery-view.component';
import * as moment from 'moment';
import { WidgetDimensions } from '../dashboard.service';
@Component({
@@ -15,14 +14,20 @@ import { WidgetDimensions } from '../dashboard.service';
templateUrl: './visualization-page.component.html',
styleUrls: ['./visualization-page.component.scss']
})
export class VisualizationPageComponent implements OnInit {
export class VisualizationPageComponent implements OnInit, AfterViewInit {
readonly DATE_PATTERN = "YYYY-MM-DD HH:mm:ss"; // for moment-JS
@Input()
defaultConfig?: PlotConfig;
@Input()
galleryEnabled = true;
dateRange = new UntypedFormControl('2019-10-05 00:00:00 - 2019-10-11 23:59:59');
selectedPlotType = new Array<PlotType>();
plotTypes: Array<any> = [];
plotTypes: PlotType[] = [];
tagFields: Array<TagField> = new Array<TagField>();
@@ -68,34 +73,69 @@ export class VisualizationPageComponent implements OnInit {
}
ngOnInit() {
const that = this;
(<any>window).initDatePicker();
this.plotTypes = this.plotService.getPlotTypes();
this.selectedPlotType.push(this.plotTypes[0]);
that.plotService.getFilterDefaults().subscribe(function(filterDefaults: FilterDefaults) {
this.plotService.getFilterDefaults().subscribe((filterDefaults: FilterDefaults) => {
filterDefaults.fields.forEach(function(name:string) {
that.tagFields.push(new TagField(name));
filterDefaults.fields.forEach((name:string) => {
this.tagFields.push(new TagField(name));
},
(error: any) => {
that.showError(error.error.message);
this.showError(error.error.message);
});
that.groupBy = that.tagFields.filter(val => filterDefaults.groupBy.includes(val.name));
that.splitBy = that.tagFields.find(val => filterDefaults.splitBy == val.name);
});
const groupByDefaults = this.defaultConfig ? this.defaultConfig.groupBy : filterDefaults.groupBy;
this.groupBy = this.tagFields.filter(val => groupByDefaults.includes(val.name));
this.splitBy = this.tagFields.find(val => filterDefaults.splitBy == val.name);
if (this.defaultConfig) {
this.plot();
}
});
}
ngAfterViewInit(): void {
if (this.defaultConfig) {
const c = this.defaultConfig;
this.query.suggestionFetcherEnabled = false;
this.query.queryField.setValue(new Suggestion(c.query, c.query, c.query.length));
this.query.suggestionFetcherEnabled = true;
this.selectedPlotType = this.plotTypes.filter(pt => c.aggregates.includes(pt.id));
this.changePlotType(this.selectedPlotType);
this.updateDateRange(c.dateRange, false);
this.limitbycomponent.limitBy = c.limitBy;
this.limitbycomponent.limit = c.limit;
this.y1AxisDefinitionComponent.yAxisScale = c.y1.axisScale;
this.y1AxisDefinitionComponent.minYValue = c.y1.rangeMin;
this.y1AxisDefinitionComponent.maxYValue = c.y1.rangeMax;
this.y1AxisDefinitionComponent.yAxisUnit = c.y1.rangeUnit;
if (c.y2) {
this.y2AxisDefinitionComponent.yAxisScale = c.y2.axisScale;
this.y2AxisDefinitionComponent.minYValue = c.y2.rangeMin;
this.y2AxisDefinitionComponent.maxYValue = c.y2.rangeMax;
this.y2AxisDefinitionComponent.yAxisUnit = c.y2.rangeUnit;
}
}
}
loading(event: LoadingEvent) {
this.plotJobActive = event.loading;
}
updateDateRange(newDateRange: string) {
updateDateRange(newDateRange: string, updatePlot=true) {
(<HTMLInputElement>document.getElementById("search-date-range")).value = newDateRange;
if (updatePlot){
this.plot();
}
}
changePlotType(selectedPlotTypes: Array<PlotType>) {
const compatiblePlotTypes = this.plotTypes.filter(pt => pt.compatible(selectedPlotTypes));

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 32 32">
<g transform="translate(0.5,0.5)" style="fill:black">
<circle cx="4" cy="9" r="2"/>
<circle cx="12" cy="9" r="2"/>
<circle cx="20" cy="9" r="2"/>
<circle cx="28" cy="9" r="2"/>
<circle cx="4" cy="15" r="2"/>
<circle cx="12" cy="15" r="2"/>
<circle cx="20" cy="15" r="2"/>
<circle cx="28" cy="15" r="2"/>
<circle cx="4" cy="21" r="2"/>
<circle cx="12" cy="21" r="2"/>
<circle cx="20" cy="21" r="2"/>
<circle cx="28" cy="21" r="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@@ -48,9 +48,6 @@
}
$( document ).ready(function() {
//initDatePicker();
initInvaders('results');
document.addEventListener("invadersPause", function(event) {
pauseInvaders();

View File

@@ -110,23 +110,25 @@ button[disabled] .icon-inline {
button.save-button {
background-color: #ff9900;
}
button.save-button:disabled {
background-color: inherit;
}
.icon-tiny {
width: 1em;
height: 1em;
}
.icon-small {
width: 1.5em;
height: 1.5em;
}
.icon-small:hover {
background-color: #eee;
}
.icon-middle {
width: 2.5em;
height: 2.5em;
margin: 0.2em;
}
.icon-middle:hover {
background-color: #eee;
}
.icon-large {
width: 8em;
@@ -145,8 +147,6 @@ button.save-button {
vertical-align: text-bottom;
}
a ,a:visited {
color: blue;
text-decoration: none;

View File

@@ -90,7 +90,7 @@ public class TcpIngestor implements Ingestor, AutoCloseable, DisposableBean {
port = serverSocket.getLocalPort();
LOGGER.info("listening on port " + serverSocket.getLocalPort());
serverSocket.setSoTimeout((int) TimeUnit.MILLISECONDS.toMillis(2));
serverSocket.setSoTimeout((int) TimeUnit.MILLISECONDS.toMillis(100));
started.countDown(); // notify caller that the socket is now listening
while (acceptNewConnections.get()) {