move dashboard elements and store the order

This commit is contained in:
2023-03-05 16:16:54 +01:00
parent 77c576c434
commit 10c982c8bc
15 changed files with 399 additions and 72 deletions

View File

@@ -6,6 +6,7 @@ import { UploadPageComponent } from './upload-page/upload-page.component';
import { HelpPageComponent } from './help-page/help-page.component';
import { DashboardPageComponent } from './dashboard-page/dashboard-page.component';
import { DashboardComponent } from './dashboard-page/dashboard/dashboard.component';
import { CustomizableGridComponent } from './customizable-grid/customizable-grid.component';
const routes: Routes = [
@@ -14,6 +15,7 @@ const routes: Routes = [
{ path: 'dashboard', component: DashboardPageComponent},
{ path: 'dashboard/:id', component: DashboardComponent},
{ path: 'upload', component: UploadPageComponent },
{ path: 'grid', component: CustomizableGridComponent },
{ path: 'help', component: HelpPageComponent },
// { path: '**', component: PageNotFoundComponent }
];

View File

@@ -33,12 +33,16 @@ 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 {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';
import { TextWidgetComponent } from './dashboard-page/dashboard/text-widget/text-widget.component';
import { AddPlotDialogComponent } from './dashboard-page/dashboard/add-plot-dialog/add-plot-dialog.component';
import { PlotWidgetComponent } from './dashboard-page/dashboard/plot-widget/plot-widget.component';
import { FullScreenPlotDialogComponent } from './dashboard-page/dashboard/full-screen-plot-dialog/full-screen-plot-dialog.component';
import { CustomizableGridComponent } from './customizable-grid/customizable-grid.component';
import {DragDropModule} from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
@@ -63,14 +67,17 @@ import { FullScreenPlotDialogComponent } from './dashboard-page/dashboard/full-s
TextWidgetComponent,
AddPlotDialogComponent,
PlotWidgetComponent,
FullScreenPlotDialogComponent
FullScreenPlotDialogComponent,
CustomizableGridComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
DragDropModule,
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatCheckboxModule,
MatDialogModule,

View File

@@ -0,0 +1,121 @@
<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 {
border: solid 1px #ccc;
min-height: 60px;
background: white;
border-radius: 4px;
overflow: hidden;
display: block;
}
.example-box {
padding: 20px 10px;
border-bottom: solid 1px #ccc;
color: rgba(0, 0, 0, 0.87);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
cursor: move;
background: white;
font-size: 14px;
}
.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);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.example-box:last-child {
border: none;
}
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
</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>
-->
<div class="example-container">
<h2>To do</h2>
<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>
<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>
</div>
</div>

View File

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

View File

@@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';
@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'];
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,
);
}
}
}

View File

@@ -1,27 +1,28 @@
<style>
:host {
/*
height: calc(100% - 29px);
*/
width: 100%;
position: absolute;
padding: 0.5em;
}
/*
height: calc(100% - 29px);
*/
width: 100%;
position: absolute;
padding: 0.5em;
}
.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.center-content {
text-align: center;
}
.is-error {
font-size: 3rem;
font-weight: bold;
color: #333;
}
.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.center-content {
text-align: center;
}
.is-error {
font-size: 3rem;
font-weight: bold;
color: #333;
}
</style>
<div *ngIf="loading" class="center">
@@ -39,6 +40,11 @@
</div>
<table *ngIf="dataSource.length > 0" mat-table [dataSource]="dataSource" >
<!-- Icon Column -->
<ng-container matColumnDef="icon">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element"><a [routerLink]="['/dashboard', element.id]"><img src="assets/img/dashboard-outline.svg"/></a></td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>

View File

@@ -10,7 +10,7 @@ import { NewDashboardComponent } from './new-dashboard/new-dashboard.component';
})
export class DashboardPageComponent implements OnInit {
displayedColumns: string[] = ['name', 'description'];
displayedColumns: string[] = [/*'icon',*/ 'name', 'description'];
dataSource: Dashboard[] = [];
loading = true;
error = "";

View File

@@ -22,6 +22,10 @@
.content {
padding: 0.5em;
}
.dashboard-column {
display: flex;
flex-direction: column;
}
</style>
<div *ngIf="dashboard === undefined && !error" class="center">
@@ -40,6 +44,25 @@
<h1>{{dashboard.name}}</h1>
<p>{{dashboard.description}}</p>
<app-text-widget *ngFor="let textWidget of dashboard.texts" [text]="textWidget.text"></app-text-widget>
<app-plot-widget *ngFor="let p of plotWidgetRenderData" [data]="p"></app-plot-widget>
<div cdkDropListGroup>
<!-- All lists in here will be connected. -->
<div
cdkDropList
class="dashboard-column"
*ngFor="let column of dashboard.arrangement"
[cdkDropListData]="column"
(cdkDropListDropped)="drop($event)">
<div
cdkDrag
*ngFor="let id of column"
[attr.widget-id]="id">
<app-text-widget
*ngIf="isTextWidget(id)"
[text]="getTextWidget(id)!.text"></app-text-widget>
<app-plot-widget
*ngIf="isPlotWidget(id)"
[data]="getPlotWidget(id)!"></app-plot-widget>
</div>
</div>
</div>
</div>

View File

@@ -1,9 +1,10 @@
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Component, ElementRef, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar';
import { ActivatedRoute } from '@angular/router';
import { Dashboard, DashboardService, PlotSize, PlotWidget, PlotWidgetRenderData, TextWidget } from 'src/app/dashboard.service';
import { BaseWidget, Dashboard, DashboardService, PlotSize, PlotWidget, PlotWidgetRenderData, TextWidget } from 'src/app/dashboard.service';
import { PlotConfig, PlotRequest, PlotResponse, PlotService, RenderOptions } from 'src/app/plot.service';
import { AddPlotDialogComponent } from './add-plot-dialog/add-plot-dialog.component';
import { AddTextDialogComponent } from './add-text-dialog/add-text-dialog.component';
@@ -25,29 +26,31 @@ export class DashboardComponent implements OnInit {
private service: DashboardService,
private dialog: MatDialog,
private snackBar: MatSnackBar,
private plotService: PlotService) {}
private plotService: PlotService,
private element: ElementRef) {}
ngOnInit(): void {
this.service.getDashboard(<string>this.route.snapshot.paramMap.get("id")).subscribe({
'next':(dashboard: Dashboard) => {
this.dashboard = dashboard;
this.dashboard = dashboard;
this.repairArrangement();
dashboard.plots.forEach(p => {
this.plotWidgetRenderData.push(new PlotWidgetRenderData(p));
});
dashboard.plots.forEach(p => {
this.plotWidgetRenderData.push(new PlotWidgetRenderData(p));
});
this.loadImages(0, this.plotWidgetRenderData);
},
'error': (error: HttpErrorResponse) =>{
if (error.status == 404) {
this.error = "Not Found";
}else if (error.status == 504) { // gateway timeout
this.error = "Server Unreachable";
}else{
this.error = "Failed to load dashboard";
this.loadImages(0, this.plotWidgetRenderData);
},
'error': (error: HttpErrorResponse) =>{
if (error.status == 404) {
this.error = "Not Found";
}else if (error.status == 504) { // gateway timeout
this.error = "Server Unreachable";
}else{
this.error = "Failed to load dashboard";
}
}
}
});
});
}
loadImages(index: number, plotWidgetQueue: PlotWidgetRenderData[]) {
@@ -84,35 +87,63 @@ export class DashboardComponent implements OnInit {
'fullScreen': new RenderOptions(fullHeight,fullWidth, false, true)
}
);
return request;
}
return request;
}
height(size: PlotSize): number{
switch (size) {
case 'SMALL':
return 300;
case 'MEDIUM':
return 400;
case 'LARGE':
return 600;
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;
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){
arrangement[0] = [];
}
this.dashboard?.texts.forEach(t => {
if (!this.arrangmentContainsId(arrangement, t.id)){
arrangement[0].push(t.id);
}
});
this.dashboard?.plots.forEach(t => {
if (!this.arrangmentContainsId(arrangement, t.id)){
arrangement[0].push(t.id);
}
});
this.dashboard!.arrangement = arrangement;
}
private arrangmentContainsId(arrangement: string[][], id: string): boolean{
for ( let i = 0; i < arrangement.length; i++){
if (arrangement[i].includes(id)) {
return true;
}
}
return false;
}
}
addText() {
this.dialog.open(AddTextDialogComponent,{
width: '600px'
}).afterClosed().subscribe((text: string) => {
this.dashboard!.texts.push(new TextWidget('MEDIUM', text));
this.dashboard!.texts.push(new TextWidget(crypto.randomUUID(),'MEDIUM', text));
});
}
@@ -121,11 +152,28 @@ export class DashboardComponent implements OnInit {
width: 'calc(100% - 1em)',
height: 'calc(100% - 1em)'
}).afterClosed().subscribe((config: PlotConfig) => {
this.dashboard!.plots.push(new PlotWidget('MEDIUM', config));
this.dashboard!.plots.push(new PlotWidget(crypto.randomUUID(), 'MEDIUM', config));
});
}
save() {
const arrangement = <string[][]>[];
const dashboardColumns = (<HTMLElement>this.element.nativeElement).querySelectorAll('.dashboard-column');
for(let i =0; i < dashboardColumns.length; i++){
const ids = [];
const column = <HTMLDivElement>dashboardColumns.item(i);
for(let c = 0; c <column.children.length; c++) {
const element = <Element>column.children.item(c)
const id = element!.getAttribute("widget-id");
if (id !== null) {
ids.push(id);
}
}
arrangement.push(ids);
}
this.dashboard!.arrangement = arrangement;
this.service.saveDashboard(this.dashboard!).subscribe({
'complete': () => {
this.snackBar.open("saved dashboard","", {
@@ -135,4 +183,37 @@ export class DashboardComponent implements OnInit {
}
});
}
isTextWidget(id: string): boolean {
return this.getTextWidget(id) !== undefined;
}
isPlotWidget(id: string): boolean {
return this.dashboard?.plots.find( x => x.id == id) !== undefined;
}
getTextWidget(id: string): TextWidget | undefined {
return this.dashboard?.texts.find( x => x.id == id);
}
getPlotWidget(id: string): PlotWidgetRenderData | undefined {
return this.plotWidgetRenderData.find( x => x.widget.id == id);
}
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,
);
}
}
s(a: any){
return JSON.stringify(a);
}
}

View File

@@ -2,6 +2,7 @@
:host {
display: block;
margin-top: 1em;
background-color: aliceblue;
}
</style>
<p *ngFor="let line of lines()">{{line}}</p>

View File

@@ -32,26 +32,36 @@ export class DashboardCreationData{
constructor(public name: string, public description: string){}
}
export interface HasId {
id: string;
}
export class Dashboard{
constructor(public id: string, public name: string, public description: string, public texts: TextWidget[], public plots: PlotWidget[]){}
constructor(
public id: string,
public name: string,
public description: string,
public texts: TextWidget[],
public plots: PlotWidget[],
public arrangement: string[][]){}
}
export class DashboardList{
constructor(public dashboards: [Dashboard]){}
}
export abstract class BaseWidget {
constructor(public type: PlotType, public size: PlotSize) {}
export abstract class BaseWidget implements HasId {
constructor(public id: string, public type: PlotType, public size: PlotSize) {}
}
export class TextWidget extends BaseWidget {
constructor(override size: PlotSize, public text: string) {
super('TEXT', size);
constructor(override id: string, override size: PlotSize, public text: string) {
super(id, 'TEXT', size);
}
}
export class PlotWidget extends BaseWidget {
constructor(override size: PlotSize, public config: PlotConfig) {
super('PLOT', size);
constructor(override id: string, override size: PlotSize, public config: PlotConfig) {
super(id, 'PLOT', size);
}
}