From d17255ecc19241ef4012d9f6157e82d6d5904a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Tue, 23 Sep 2025 10:11:35 +0200 Subject: [PATCH] UI: TopicList + SeriesList --- src/main/angular/src/app/COMMON.ts | 52 ++++++ src/main/angular/src/app/app.html | 2 + src/main/angular/src/app/app.routes.ts | 4 + src/main/angular/src/app/common/Value.ts | 29 ++++ .../app/common/column/column.component.html | 12 ++ .../app/common/column/column.component.less | 3 + .../src/app/common/column/column.component.ts | 30 ++++ .../angular/src/app/common/sorter/Column.ts | 26 +++ .../src/app/common/sorter/Direction.ts | 4 + .../angular/src/app/common/sorter/Order.ts | 20 +++ .../angular/src/app/common/sorter/Sorter.ts | 65 ++++++++ src/main/angular/src/app/config/Config.ts | 27 +++ .../src/app/config/SeriesListConfig.ts | 34 ++++ .../angular/src/app/config/TopicListConfig.ts | 25 +++ .../angular/src/app/config/config.service.ts | 24 +++ .../src/app/plot/plot/plot.component.ts | 5 +- .../src/app/series/AllSeriesPointRequest.ts | 27 +++ .../src/app/series/AllSeriesPointResponse.ts | 74 +++++++++ src/main/angular/src/app/series/Series.ts | 19 ++- .../angular/src/app/series/SeriesPoint.ts | 40 +++++ .../angular/src/app/series/SeriesSorter.ts | 45 +++++ .../series/list/series-list.component.html | 83 ++++++++++ .../series/list/series-list.component.less | 93 +++++++++++ .../app/series/list/series-list.component.ts | 155 ++++++++++++++++++ .../angular/src/app/series/series.service.ts | 11 +- .../angular/src/app/series/varying/Varying.ts | 26 +++ .../src/app/series/varying/varying-service.ts | 28 +--- .../angular/src/app/topic/TimestampType.ts | 4 + src/main/angular/src/app/topic/Topic.ts | 44 +++++ src/main/angular/src/app/topic/TopicQuery.ts | 28 ++++ .../src/app/topic/TopicQueryFunction.ts | 5 + .../app/topic/list/topic-list.component.html | 33 ++++ .../app/topic/list/topic-list.component.less | 104 ++++++++++++ .../app/topic/list/topic-list.component.ts | 91 ++++++++++ .../angular/src/app/topic/topic.service.ts | 16 ++ src/main/angular/src/config.less | 2 + src/main/java/de/ph87/data/config/Config.java | 85 ++++++++++ .../de/ph87/data/config/ConfigController.java | 30 ++++ .../java/de/ph87/data/config/ConfigDto.java | 28 ++++ .../de/ph87/data/config/ConfigRepository.java | 11 ++ .../ph87/data/config/ConfigSeriesListDto.java | 52 ++++++ .../de/ph87/data/config/ConfigService.java | 26 +++ .../ph87/data/config/ConfigTopicListDto.java | 47 ++++++ .../java/de/ph87/data/config/Direction.java | 5 + src/main/java/de/ph87/data/config/Order.java | 37 +++++ .../java/de/ph87/data/config/OrderDto.java | 29 ++++ .../data/series/AllSeriesPointRequest.java | 34 ++++ .../data/series/AllSeriesPointResponse.java | 29 ++++ .../ph87/data/series/ISeriesPointRequest.java | 19 +++ ...quest.java => OneSeriesPointsRequest.java} | 4 +- .../data/series/OneSeriesPointsResponse.java | 14 ++ .../OneSeriesPointsResponseSerializer.java | 22 +++ .../de/ph87/data/series/SeriesController.java | 12 +- .../java/de/ph87/data/series/SeriesPoint.java | 2 - .../data/series/SeriesPointSerializer.java | 18 -- .../de/ph87/data/series/SeriesService.java | 21 ++- .../data/series/data/bool/BoolService.java | 6 +- .../data/series/data/delta/DeltaService.java | 18 +- .../series/data/varying/VaryingService.java | 18 +- src/main/java/de/ph87/data/topic/Topic.java | 23 ++- .../java/de/ph87/data/topic/TopicDto.java | 13 ++ .../de/ph87/data/topic/TopicReceiver.java | 71 ++++---- .../java/de/ph87/data/topic/TopicService.java | 5 + 63 files changed, 1852 insertions(+), 117 deletions(-) create mode 100644 src/main/angular/src/app/common/Value.ts create mode 100644 src/main/angular/src/app/common/column/column.component.html create mode 100644 src/main/angular/src/app/common/column/column.component.less create mode 100644 src/main/angular/src/app/common/column/column.component.ts create mode 100644 src/main/angular/src/app/common/sorter/Column.ts create mode 100644 src/main/angular/src/app/common/sorter/Direction.ts create mode 100644 src/main/angular/src/app/common/sorter/Order.ts create mode 100644 src/main/angular/src/app/common/sorter/Sorter.ts create mode 100644 src/main/angular/src/app/config/Config.ts create mode 100644 src/main/angular/src/app/config/SeriesListConfig.ts create mode 100644 src/main/angular/src/app/config/TopicListConfig.ts create mode 100644 src/main/angular/src/app/config/config.service.ts create mode 100644 src/main/angular/src/app/series/AllSeriesPointRequest.ts create mode 100644 src/main/angular/src/app/series/AllSeriesPointResponse.ts create mode 100644 src/main/angular/src/app/series/SeriesPoint.ts create mode 100644 src/main/angular/src/app/series/SeriesSorter.ts create mode 100644 src/main/angular/src/app/series/list/series-list.component.html create mode 100644 src/main/angular/src/app/series/list/series-list.component.less create mode 100644 src/main/angular/src/app/series/list/series-list.component.ts create mode 100644 src/main/angular/src/app/series/varying/Varying.ts create mode 100644 src/main/angular/src/app/topic/TimestampType.ts create mode 100644 src/main/angular/src/app/topic/Topic.ts create mode 100644 src/main/angular/src/app/topic/TopicQuery.ts create mode 100644 src/main/angular/src/app/topic/TopicQueryFunction.ts create mode 100644 src/main/angular/src/app/topic/list/topic-list.component.html create mode 100644 src/main/angular/src/app/topic/list/topic-list.component.less create mode 100644 src/main/angular/src/app/topic/list/topic-list.component.ts create mode 100644 src/main/angular/src/app/topic/topic.service.ts create mode 100644 src/main/angular/src/config.less create mode 100644 src/main/java/de/ph87/data/config/Config.java create mode 100644 src/main/java/de/ph87/data/config/ConfigController.java create mode 100644 src/main/java/de/ph87/data/config/ConfigDto.java create mode 100644 src/main/java/de/ph87/data/config/ConfigRepository.java create mode 100644 src/main/java/de/ph87/data/config/ConfigSeriesListDto.java create mode 100644 src/main/java/de/ph87/data/config/ConfigService.java create mode 100644 src/main/java/de/ph87/data/config/ConfigTopicListDto.java create mode 100644 src/main/java/de/ph87/data/config/Direction.java create mode 100644 src/main/java/de/ph87/data/config/Order.java create mode 100644 src/main/java/de/ph87/data/config/OrderDto.java create mode 100644 src/main/java/de/ph87/data/series/AllSeriesPointRequest.java create mode 100644 src/main/java/de/ph87/data/series/AllSeriesPointResponse.java create mode 100644 src/main/java/de/ph87/data/series/ISeriesPointRequest.java rename src/main/java/de/ph87/data/series/{SeriesPointsRequest.java => OneSeriesPointsRequest.java} (90%) create mode 100644 src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java create mode 100644 src/main/java/de/ph87/data/series/OneSeriesPointsResponseSerializer.java delete mode 100644 src/main/java/de/ph87/data/series/SeriesPointSerializer.java diff --git a/src/main/angular/src/app/COMMON.ts b/src/main/angular/src/app/COMMON.ts index bc2a7fd..5aa3ddc 100644 --- a/src/main/angular/src/app/COMMON.ts +++ b/src/main/angular/src/app/COMMON.ts @@ -3,6 +3,7 @@ import {filter, map, Subject, Subscription} from "rxjs"; import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {RxStompState} from '@stomp/rx-stomp'; +import {formatNumber} from '@angular/common'; export type FromJson = (json: any) => T; @@ -288,3 +289,54 @@ export function UTN(v: T | null | undefined): T | null { } return v; } + +export function ageString(event: Date | null, now: Date, long: boolean = true) { + if (event === null) { + return '-'; + } + const secondsTotal = Math.max(0, (now.getTime() - event.getTime()) / 1000); + + const minutesTotal = secondsTotal / 60; + const hoursTotal = minutesTotal / 60; + const daysTotal = hoursTotal / 24; + const yearsTotal = daysTotal / 365; + + const secondsPart = secondsTotal % 60; + const minutesPart = minutesTotal % 60; + const hoursPart = hoursTotal % 24; + const daysPart = daysTotal % 365; + + const locale = 'de-DE'; + const longDigits = "2.0-0"; + const shortDigits = "0.0-0"; + if (yearsTotal >= 1) { + if (long) { + return `${formatNumber(yearsTotal, locale, shortDigits)}y ${formatNumber(daysPart, locale, longDigits)}d ${formatNumber(hoursPart, locale, longDigits)}h ${formatNumber(minutesPart, locale, longDigits)}m ${formatNumber(secondsPart, locale, longDigits)}s`; + } else { + return `${formatNumber(yearsTotal, locale, shortDigits)}y ${formatNumber(daysPart, locale, shortDigits)}d`; + } + } + if (daysTotal >= 1) { + if (long) { + return `${formatNumber(daysPart, locale, shortDigits)}d ${formatNumber(hoursPart, locale, longDigits)}h ${formatNumber(minutesPart, locale, longDigits)}m ${formatNumber(secondsPart, locale, longDigits)}s`; + } else { + return `${formatNumber(daysPart, locale, shortDigits)}d ${formatNumber(hoursPart, locale, shortDigits)}h`; + } + } + if (hoursTotal >= 1) { + if (long) { + return `${formatNumber(hoursPart, locale, shortDigits)}h ${formatNumber(minutesPart, locale, longDigits)}m ${formatNumber(secondsPart, locale, longDigits)}s`; + } else { + return `${formatNumber(hoursPart, locale, shortDigits)}h ${formatNumber(minutesPart, locale, shortDigits)}m`; + } + } + if (minutesTotal >= 1) { + if (long) { + return `${formatNumber(minutesPart, locale, shortDigits)}m ${formatNumber(secondsPart, locale, longDigits)}s`; + } else { + return `${formatNumber(minutesPart, locale, shortDigits)}m ${formatNumber(secondsPart, locale, shortDigits)}s`; + } + } + return `${formatNumber(secondsPart, locale, shortDigits)}s`; +} + diff --git a/src/main/angular/src/app/app.html b/src/main/angular/src/app/app.html index cd9cf18..74ba95d 100644 --- a/src/main/angular/src/app/app.html +++ b/src/main/angular/src/app/app.html @@ -3,6 +3,8 @@
Dashboard
Diagramm-Editor
+
Topics
+
Messreihen
}
diff --git a/src/main/angular/src/app/app.routes.ts b/src/main/angular/src/app/app.routes.ts index c541937..55f36d7 100644 --- a/src/main/angular/src/app/app.routes.ts +++ b/src/main/angular/src/app/app.routes.ts @@ -1,9 +1,13 @@ import {Routes} from '@angular/router'; import {PlotEditor} from './plot/editor/plot-editor.component'; import {DashboardComponent} from './dashboard/dashboard.component'; +import {TopicListComponent} from './topic/list/topic-list.component'; +import {SeriesListComponent} from './series/list/series-list.component'; export const routes: Routes = [ {path: 'Dashboard', component: DashboardComponent}, + {path: 'TopicList', component: TopicListComponent}, + {path: 'SeriesList', component: SeriesListComponent}, {path: 'PlotEditor', component: PlotEditor}, {path: 'PlotEditor/:id', component: PlotEditor}, {path: '**', redirectTo: 'Dashboard'}, diff --git a/src/main/angular/src/app/common/Value.ts b/src/main/angular/src/app/common/Value.ts new file mode 100644 index 0000000..0a1a5fb --- /dev/null +++ b/src/main/angular/src/app/common/Value.ts @@ -0,0 +1,29 @@ +export class Value { + + readonly integer: string; + + readonly fraction: string; + + readonly showDot: boolean; + + constructor( + readonly value: number | null, + decimals: number, + ) { + if (value !== null) { + const parts = (value + '').split('.'); + if (parts.length === 2) { + this.integer = parts[0]; + this.fraction = parts[1].substring(0, decimals).padEnd(decimals, '0'); + } else { + this.integer = parts[0]; + this.fraction = '0'.repeat(decimals); + } + } else { + this.integer = '-'; + this.fraction = ''; + } + this.showDot = value !== null && decimals > 0; + } + +} diff --git a/src/main/angular/src/app/common/column/column.component.html b/src/main/angular/src/app/common/column/column.component.html new file mode 100644 index 0000000..eb28ef0 --- /dev/null +++ b/src/main/angular/src/app/common/column/column.component.html @@ -0,0 +1,12 @@ +
+ {{ title === null ? column.title : title }} + @if (sorter.isAscending(column)) { + + } + @if (sorter.isDescending(column)) { + + } + @if (sorter.showCount(column)) { + {{ sorter.indexOf(column) + 1 }} + } +
diff --git a/src/main/angular/src/app/common/column/column.component.less b/src/main/angular/src/app/common/column/column.component.less new file mode 100644 index 0000000..0171ed4 --- /dev/null +++ b/src/main/angular/src/app/common/column/column.component.less @@ -0,0 +1,3 @@ +.sub { + font-size: 50%; +} diff --git a/src/main/angular/src/app/common/column/column.component.ts b/src/main/angular/src/app/common/column/column.component.ts new file mode 100644 index 0000000..d92876d --- /dev/null +++ b/src/main/angular/src/app/common/column/column.component.ts @@ -0,0 +1,30 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {Column} from '../sorter/Column'; +import {Sorter} from '../sorter/Sorter'; + +@Component({ + selector: 'th[app-column]', + imports: [], + templateUrl: './column.component.html', + styleUrl: './column.component.less' +}) +export class ColumnComponent { + + @Input() + column!: Column; + + @Input() + sorter!: Sorter; + + @Input() + title: string | null = null; + + @Output() + readonly onChange: EventEmitter = new EventEmitter(); + + click() { + this.sorter.toggle(this.column); + this.onChange.emit(); + } + +} diff --git a/src/main/angular/src/app/common/sorter/Column.ts b/src/main/angular/src/app/common/sorter/Column.ts new file mode 100644 index 0000000..124d15f --- /dev/null +++ b/src/main/angular/src/app/common/sorter/Column.ts @@ -0,0 +1,26 @@ +import {Compare} from '../../COMMON'; + +export class Column { + + private constructor( + readonly name: string, + readonly title: string, + readonly compare: Compare, + ) { + // + } + + static create( + name: string, + title: string, + getter: (value: T) => R, + compare: Compare, + ) { + return new Column( + name, + title, + (a, b) => compare(getter(a), getter(b)), + ); + } + +} diff --git a/src/main/angular/src/app/common/sorter/Direction.ts b/src/main/angular/src/app/common/sorter/Direction.ts new file mode 100644 index 0000000..5a7283d --- /dev/null +++ b/src/main/angular/src/app/common/sorter/Direction.ts @@ -0,0 +1,4 @@ +export enum Direction { + ASC = "ASC", + DESC = "DESC", +} diff --git a/src/main/angular/src/app/common/sorter/Order.ts b/src/main/angular/src/app/common/sorter/Order.ts new file mode 100644 index 0000000..393adc2 --- /dev/null +++ b/src/main/angular/src/app/common/sorter/Order.ts @@ -0,0 +1,20 @@ +import {Column} from "./Column"; +import {Direction} from './Direction'; + +export class Order { + + constructor( + public column: Column, + public direction: Direction = Direction.ASC, + ) { + // + } + + toJson(): {} { + return { + property: this.column.name, + direction: this.direction, + }; + } + +} diff --git a/src/main/angular/src/app/common/sorter/Sorter.ts b/src/main/angular/src/app/common/sorter/Sorter.ts new file mode 100644 index 0000000..984e04d --- /dev/null +++ b/src/main/angular/src/app/common/sorter/Sorter.ts @@ -0,0 +1,65 @@ +import {Order} from "./Order"; +import {Column} from "./Column"; +import {Direction} from './Direction'; + +export abstract class Sorter { + + protected constructor( + private _orders: Order[] = [], + ) { + // + } + + get orders(): Order[] { + return this._orders; + } + + readonly compare = (a: T, b: T) => { + for (const order of this._orders) { + const diff = order.column.compare(a, b); + if (diff !== 0) { + return order.direction === Direction.ASC ? diff : -diff; + } + } + return 0; + } + + toggle(column: Column): void { + const index = this.indexOf(column); + if (index < 0) { + this.orders.push(new Order(column)); + } else { + const order = this.orders[index]; + if (order.direction === Direction.ASC) { + order.direction = Direction.DESC; + } else if (order.direction === Direction.DESC) { + this.orders.splice(index, 1); + } + } + } + + get count(): number { + return this._orders.length; + } + + toJson(): {} { + return this.orders.map(o => o.toJson()); + } + + isAscending(column: Column): boolean { + return this.orders.some(o => o.column === column && o.direction === Direction.ASC); + } + + isDescending(column: Column): boolean { + return this.orders.some(o => o.column === column && o.direction === Direction.DESC); + } + + indexOf(column: Column) { + return this.orders.findIndex(o => o.column === column); + } + + showCount(column: Column): boolean { + return this.count > 1 && this.orders.some(o => o.column === column); + } + +} diff --git a/src/main/angular/src/app/config/Config.ts b/src/main/angular/src/app/config/Config.ts new file mode 100644 index 0000000..74ddf28 --- /dev/null +++ b/src/main/angular/src/app/config/Config.ts @@ -0,0 +1,27 @@ +import {TopicListConfig} from './TopicListConfig'; +import {SeriesListConfig} from './SeriesListConfig'; + +export class Config { + + constructor( + public topicList: TopicListConfig = new TopicListConfig(), + public seriesList: SeriesListConfig = new SeriesListConfig(), + ) { + // + } + + toJson(): {} { + return { + topicList: this.topicList, + seriesList: this.seriesList.toJson(), + }; + } + + static fromJson(json: any): Config { + return new Config( + TopicListConfig.fromJson(json.topicList), + SeriesListConfig.fromJson(json.seriesList), + ); + } + +} diff --git a/src/main/angular/src/app/config/SeriesListConfig.ts b/src/main/angular/src/app/config/SeriesListConfig.ts new file mode 100644 index 0000000..995bdb5 --- /dev/null +++ b/src/main/angular/src/app/config/SeriesListConfig.ts @@ -0,0 +1,34 @@ +import {Interval} from '../series/Interval'; +import {SeriesSorter} from '../series/SeriesSorter'; +import {mapNotNull, validateNumber, validateString} from '../COMMON'; + +export class SeriesListConfig { + + constructor( + public interval: Interval | null = null, + public offset: number = 0, + public search: string = "", + public sorter: SeriesSorter = new SeriesSorter(), + ) { + // + } + + toJson(): {} { + return { + interval: this.interval?.name, + offset: this.offset, + search: this.search, + orders: this.sorter.toJson(), + }; + } + + static fromJson(json: any): SeriesListConfig { + return new SeriesListConfig( + mapNotNull(json.interval, Interval.fromJson), + validateNumber(json.offset), + validateString(json.search), + SeriesSorter.fromJson(json.orders), + ); + } + +} diff --git a/src/main/angular/src/app/config/TopicListConfig.ts b/src/main/angular/src/app/config/TopicListConfig.ts new file mode 100644 index 0000000..e5bed42 --- /dev/null +++ b/src/main/angular/src/app/config/TopicListConfig.ts @@ -0,0 +1,25 @@ +import {validateBoolean} from '../COMMON'; + +export class TopicListConfig { + + constructor( + public hidden: boolean = true, + public used: boolean = true, + public unused: boolean = true, + public ok: boolean = true, + public details: boolean = false, + ) { + // + } + + static fromJson(json: any): TopicListConfig { + return new TopicListConfig( + validateBoolean(json.hidden), + validateBoolean(json.used), + validateBoolean(json.unused), + validateBoolean(json.ok), + validateBoolean(json.details), + ); + } + +} diff --git a/src/main/angular/src/app/config/config.service.ts b/src/main/angular/src/app/config/config.service.ts new file mode 100644 index 0000000..3ea5ddc --- /dev/null +++ b/src/main/angular/src/app/config/config.service.ts @@ -0,0 +1,24 @@ +import {Injectable} from '@angular/core'; +import {Config} from './Config'; +import {ApiService, Next} from '../COMMON'; + +@Injectable({ + providedIn: 'root' +}) +export class ConfigService { + + constructor( + readonly api: ApiService, + ) { + // + } + + get(next: Next) { + this.api.getSingle(["Config", "get"], Config.fromJson, next); + } + + set(config: Config, next: Next) { + this.api.postSingle(["Config", "set"], config.toJson(), Config.fromJson, next); + } + +} diff --git a/src/main/angular/src/app/plot/plot/plot.component.ts b/src/main/angular/src/app/plot/plot/plot.component.ts index 10651ae..0a0520d 100644 --- a/src/main/angular/src/app/plot/plot/plot.component.ts +++ b/src/main/angular/src/app/plot/plot/plot.component.ts @@ -11,10 +11,11 @@ import {Axis} from '../axis/Axis'; import {Subscription} from 'rxjs'; import {Delta, DeltaService} from '../../series/delta/delta-service'; import {Bool, BoolService} from '../../series/bool/bool-service'; -import {Varying, VaryingService} from '../../series/varying/varying-service'; +import {VaryingService} from '../../series/varying/varying-service'; import {Interval} from '../../series/Interval'; import {SeriesService} from '../../series/series.service'; import {GraphType} from '../axis/graph/GraphType'; +import {Varying} from '../../series/varying/Varying'; type Dataset = ChartDataset[][number]; @@ -213,7 +214,7 @@ export class PlotComponent implements AfterViewInit, OnDestroy { } private points(graph: Graph, min: Dataset, mid: Dataset, max: Dataset): void { - this.seriesService.points( + this.seriesService.oneSeriesPoints( graph.series, graph.axis.plot.interval, graph.axis.plot.offset, diff --git a/src/main/angular/src/app/series/AllSeriesPointRequest.ts b/src/main/angular/src/app/series/AllSeriesPointRequest.ts new file mode 100644 index 0000000..083512b --- /dev/null +++ b/src/main/angular/src/app/series/AllSeriesPointRequest.ts @@ -0,0 +1,27 @@ +import {Interval} from "./Interval"; +import {validateNumber} from '../COMMON'; + +export class AllSeriesPointRequest { + + constructor( + readonly interval: Interval, + readonly offset: number, + ) { + // + } + + toJson(): {} { + return { + interval: this.interval.name, + offset: this.offset, + }; + } + + static fromJson(json: any): AllSeriesPointRequest { + return new AllSeriesPointRequest( + Interval.fromJson(json.interval), + validateNumber(json.offset), + ); + } + +} diff --git a/src/main/angular/src/app/series/AllSeriesPointResponse.ts b/src/main/angular/src/app/series/AllSeriesPointResponse.ts new file mode 100644 index 0000000..0add006 --- /dev/null +++ b/src/main/angular/src/app/series/AllSeriesPointResponse.ts @@ -0,0 +1,74 @@ +import {mapNotNull, validateBoolean, validateDate, validateList, validateNumber} from "../COMMON"; +import {AllSeriesPointRequest} from './AllSeriesPointRequest'; +import {Series} from './Series'; +import {Value} from '../common/Value'; + +export class AllSeriesPointResponseEntry { + + readonly min: Value; + + readonly avg: Value; + + readonly max: Value; + + readonly first: Value; + + readonly last: Value; + + constructor( + readonly series: Series, + min: number | null, + avg: number | null, + max: number | null, + first: number | null, + last: number | null, + readonly state: boolean | null, + readonly terminated: boolean | null, + ) { + this.min = new Value(min, series.decimals); + this.max = new Value(max, series.decimals); + this.first = new Value(max, series.decimals); + this.last = new Value(max, series.decimals); + if (avg !== null) { + this.avg = new Value(avg, series.decimals); + } else if (first !== null && last !== null) { + this.avg = new Value(last - first, series.decimals); + } else { + this.avg = new Value(null, series.decimals); + } + } + + static fromJson(json: any): AllSeriesPointResponseEntry { + return new AllSeriesPointResponseEntry( + Series.fromJson(json.series), + mapNotNull(json.point?.min, validateNumber), + mapNotNull(json.point?.avg, validateNumber), + mapNotNull(json.point?.max, validateNumber), + mapNotNull(json.point?.first, validateNumber), + mapNotNull(json.point?.last, validateNumber), + mapNotNull(json.state, validateBoolean), + mapNotNull(json.terminated, validateBoolean), + ); + } + +} + +export class AllSeriesPointResponse { + + constructor( + readonly request: AllSeriesPointRequest, + readonly date: Date, + readonly seriesPoints: AllSeriesPointResponseEntry[], + ) { + // + } + + static fromJson(json: any): AllSeriesPointResponse { + return new AllSeriesPointResponse( + AllSeriesPointRequest.fromJson(json.request), + validateDate(json.request?.first), + validateList(json.seriesPoints, AllSeriesPointResponseEntry.fromJson), + ); + } + +} diff --git a/src/main/angular/src/app/series/Series.ts b/src/main/angular/src/app/series/Series.ts index 591cf4f..1558d3b 100644 --- a/src/main/angular/src/app/series/Series.ts +++ b/src/main/angular/src/app/series/Series.ts @@ -1,9 +1,12 @@ -import {ID, mapNotNull, validateBoolean, validateNumber, validateString} from "../COMMON"; +import {ID, mapNotNull, validateBoolean, validateDate, validateNumber, validateString} from "../COMMON"; import {SeriesType} from './SeriesType'; import {formatNumber} from '@angular/common'; +import {Value} from '../common/Value'; export class Series extends ID { + readonly value: Value; + constructor( readonly id: number, readonly version: number, @@ -12,9 +15,12 @@ export class Series extends ID { readonly unit: string, readonly type: SeriesType, readonly decimals: number, - readonly value: number | null, + readonly expectedEverySeconds: number, + readonly date: Date | null, + value: number | null, ) { super(); + this.value = new Value(value, this.decimals); } static fromJson(json: any): Series { @@ -26,6 +32,8 @@ export class Series extends ID { validateString(json.unit), validateString(json.type) as SeriesType, validateNumber(json.decimals), + validateNumber(json.expectedEverySeconds), + mapNotNull(json.last, validateDate), mapNotNull(json.value, validateNumber), ); } @@ -35,7 +43,7 @@ export class Series extends ID { } get valueString(): string { - const result = (this.value === null ? "-" : this.type === SeriesType.BOOL ? this.value > 0 ? "EIN" : "AUS" : formatNumber(this.value, "de-DE", this.digitString)) + ""; + const result = (this.value.value === null ? "-" : this.type === SeriesType.BOOL ? this.value.value > 0 ? "EIN" : "AUS" : formatNumber(this.value.value, "de-DE", this.digitString)) + ""; if (this.unit) { return result + " " + this.unit; } @@ -46,7 +54,8 @@ export class Series extends ID { return a.name.localeCompare(b.name); } - equals2() { - + isOld(now: Date, toleranceSeconds: number) { + return (now?.getTime() - (this.date?.getTime() || 0)) > (this.expectedEverySeconds + toleranceSeconds) * 1000; } + } diff --git a/src/main/angular/src/app/series/SeriesPoint.ts b/src/main/angular/src/app/series/SeriesPoint.ts new file mode 100644 index 0000000..5474e15 --- /dev/null +++ b/src/main/angular/src/app/series/SeriesPoint.ts @@ -0,0 +1,40 @@ +import {Series} from "./Series"; +import {mapNotNull, validateBoolean, validateNumber} from "../COMMON"; +import {Value} from '../common/Value'; + +export class SeriesPoint { + + readonly min: Value; + + readonly avg: Value; + + readonly max: Value; + + readonly delta: Value; + + constructor( + readonly series: Series, + min: number | null, + avg: number | null, + max: number | null, + delta: number | null, + readonly state: boolean | null, + ) { + this.min = new Value(min, series.decimals); + this.avg = new Value(avg, series.decimals); + this.max = new Value(max, series.decimals); + this.delta = new Value(delta, series.decimals); + } + + static fromJson(json: any): SeriesPoint { + return new SeriesPoint( + Series.fromJson(json), + mapNotNull(json.min, validateNumber), + mapNotNull(json.avg, validateNumber), + mapNotNull(json.max, validateNumber), + mapNotNull(json.delta, validateNumber), + mapNotNull(json.state, validateBoolean), + ) + } + +} diff --git a/src/main/angular/src/app/series/SeriesSorter.ts b/src/main/angular/src/app/series/SeriesSorter.ts new file mode 100644 index 0000000..718f491 --- /dev/null +++ b/src/main/angular/src/app/series/SeriesSorter.ts @@ -0,0 +1,45 @@ +import {Series} from "./Series"; +import {Sorter} from '../common/sorter/Sorter'; +import {Column} from '../common/sorter/Column'; +import {Order} from '../common/sorter/Order'; +import {Direction} from '../common/sorter/Direction'; +import {compareDates, compareNullable, compareNumbers, compareStrings, validateList, validateString} from '../COMMON'; + +export class SeriesSorter extends Sorter { + + static readonly nam: Column = Column.create("name", "Name", s => s.name, compareStrings); + + static readonly icon: Column = Column.create("icon", "Icon", s => s.name, compareStrings); + + static readonly type: Column = Column.create("delta", "Type", s => s.type, compareStrings); + + static readonly unit: Column = Column.create("unit", "Einheit", s => s.unit, compareStrings); + + static readonly date: Column = Column.create("first", "Zuerst", s => s.date, compareNullable(compareDates)); + + static readonly decimals: Column = Column.create("decimals", "Stellen", s => s.decimals, compareNumbers); + + static readonly value: Column = Column.create("value", "Wert", s => s.value.value, compareNullable(compareNumbers)); + + static COLUMNS: Column[] = [this.nam, this.icon, this.type, this.unit, this.date, this.decimals, this.value]; + + private static getColumn(name: string): Column { + return SeriesSorter.COLUMNS.filter(c => c.name === name)[0]; + } + + static fromJson(json: any): SeriesSorter { + return new SeriesSorter( + validateList(json, j => new Order( + SeriesSorter.getColumn(j.property), + validateString(j.direction) as Direction, + )) + ); + } + + constructor( + orders: Order[] = [], + ) { + super(orders); + } + +} diff --git a/src/main/angular/src/app/series/list/series-list.component.html b/src/main/angular/src/app/series/list/series-list.component.html new file mode 100644 index 0000000..d9c5c6f --- /dev/null +++ b/src/main/angular/src/app/series/list/series-list.component.html @@ -0,0 +1,83 @@ +
+
Live
+
5 Min.
+
Stunde
+
Tag
+
Woche
+
Monat
+
Jahr
+
+ +
+
-7
+
-6
+
-5
+
-4
+
-3
+
-2
+
-1
+
0
+
+ + + + + + + + + + + + + @if (config.seriesList.interval === null) { + @for (series of seriesFiltered; track series.id) { + + + + @if (series.value.showDot) { + + } @else { + + } + + + @switch (series.type) { + @case (SeriesType.VARYING) { + + } + @case (SeriesType.DELTA) { + + } + @case (SeriesType.BOOL) { + + } + } + + } + } + + @if (config.seriesList.interval !== null) { + @for (seriesPoint of aggregateFiltered; track seriesPoint.series.id) { + + + + @if (seriesPoint.avg.showDot) { + + } @else { + + } + + + + + } + } + +
{{ series.name }}{{ series.value.integer }},{{ series.value.fraction }}{{ series.unit }}ΔB
{{ seriesPoint.series.name }}{{ seriesPoint.avg.integer }},{{ seriesPoint.avg.fraction }}{{ seriesPoint.series.unit }}Δ
+ +@if (config.seriesList.interval === null) { +
{{ seriesFiltered.length }} / {{ seriesUnfiltered.length }}
+} @else { +
{{ aggregateFiltered.length }} / {{ aggregateUnfiltered.length }}
+} diff --git a/src/main/angular/src/app/series/list/series-list.component.less b/src/main/angular/src/app/series/list/series-list.component.less new file mode 100644 index 0000000..e1b315f --- /dev/null +++ b/src/main/angular/src/app/series/list/series-list.component.less @@ -0,0 +1,93 @@ +@import "../../../config"; + +.ChoiceList { + display: flex; + user-select: none; + border-left: @border solid gray; + border-right: @border solid gray; + border-bottom: @border solid gray; + + .Choice { + flex: 1; + padding: @space; + border-left: @border solid gray; + border-left: @border solid gray; + text-align: center; + } + + .Choice:first-child { + border: none; + } + + .ChoiceActive { + background-color: lightskyblue; + } + +} + +.ChoiceListInactive { + opacity: 20%; +} + +table { + width: 100%; + white-space: nowrap; + + th { + user-select: none; + } + + td, th { + text-align: center; + width: 0; + padding: @space calc(2 * @space); + } + + tr.Series:nth-child(even) { + background-color: #fff6e9; + } + + tr.Series:nth-child(odd) { + background-color: #fffce9; + } + + .name { + text-align: left; + width: 100%; + } + + .value { + text-align: right; + padding-right: 0; + } + + .valueInteger { + padding-right: 0; + text-align: right; + } + + .valueDot { + padding-left: 0; + padding-right: 0; + } + + .valueFraction { + padding-left: 0; + padding-right: 0; + text-align: left; + } + + .unit { + padding-left: @space; + text-align: left; + } + + .SeriesOld { + color: gray; + } + +} + +.Counts { + text-align: right; +} diff --git a/src/main/angular/src/app/series/list/series-list.component.ts b/src/main/angular/src/app/series/list/series-list.component.ts new file mode 100644 index 0000000..f1b4aae --- /dev/null +++ b/src/main/angular/src/app/series/list/series-list.component.ts @@ -0,0 +1,155 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Config} from '../../config/Config'; +import {Subscription, timer} from 'rxjs'; +import {ConfigService} from '../../config/config.service'; +import {SeriesService} from '../series.service'; +import {Series} from '../Series'; +import {ColumnComponent} from '../../common/column/column.component'; + +import {SeriesSorter} from '../SeriesSorter'; +import {FormsModule} from '@angular/forms'; +import {Interval} from '../Interval'; +import {SeriesType} from '../SeriesType'; +import {AllSeriesPointResponse, AllSeriesPointResponseEntry} from '../AllSeriesPointResponse'; +import {Varying} from '../varying/Varying'; + +@Component({ + selector: 'app-series-list', + imports: [ + ColumnComponent, + FormsModule + ], + templateUrl: './series-list.component.html', + styleUrl: './series-list.component.less' +}) +export class SeriesListComponent implements OnInit, OnDestroy { + + protected readonly Series = Series; + + protected readonly Interval = Interval; + + protected readonly SeriesType = SeriesType; + + protected readonly SeriesSorter = SeriesSorter; + + protected now: Date = new Date(); + + protected seriesUnfiltered: Series[] = []; + + protected seriesFiltered: Series[] = []; + + protected aggregateResponse: AllSeriesPointResponse | null = null; + + protected aggregateUnfiltered: AllSeriesPointResponseEntry[] = []; + + protected aggregateFiltered: AllSeriesPointResponseEntry[] = []; + + protected config: Config = new Config(); + + private readonly subs: Subscription[] = []; + + private saveConfigTimeout: any = undefined; + + constructor( + readonly configService: ConfigService, + readonly seriesService: SeriesService, + ) { + // + } + + ngOnInit(): void { + this.configService.get(config => { + this.config = config; + this.fetchList(); + this.subs.push(this.seriesService.subscribe(this.updateSeries)); + this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date())); + }); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + + private readonly updateSeries = (series: Series) => { + const index = this.seriesUnfiltered.findIndex(t => t.id === series.id); + if (index >= 0) { + this.seriesUnfiltered.splice(index, 1, series); + } else { + this.seriesUnfiltered.push(series); + } + this.applySeriesFilter(); + }; + + saveConfig(fetch: boolean) { + if (this.saveConfigTimeout) { + clearTimeout(this.saveConfigTimeout); + this.saveConfigTimeout = undefined; + } + if (fetch) { + this.fetchList(); + } else { + this.applySeriesFilter(); + this.applyAggregateFilter(); + } + this.saveConfigTimeout = setTimeout(() => this.configService.set(this.config, config => this.config = config), 700); + } + + private readonly filter = (series: Series) => { + const words = this.config.seriesList.search + .replaceAll(/([0-9])([a-zA-Z])/g, '$1 $2') + .replaceAll(/([a-z])([A-Z0-9])/g, '$1 $2') + .replaceAll(/([A-Z])([0-9])/g, '$1 $2') + .toLocaleLowerCase() + .trim() + .split(/\W+/); + return words.every(word => + series.name.toLocaleLowerCase().includes(word) + || series.unit.toLocaleLowerCase().includes(word) + || series.value.integer.includes(word) + || series.name.toLocaleLowerCase().includes(word) + || (series.type === SeriesType.DELTA && "delta difference".includes(word)) + || (series.type === SeriesType.VARYING && "average avg minimum maximum".includes(word)) + || (series.type === SeriesType.BOOL && "boolean".includes(word)) + ); + } + + protected setInterval(interval: Interval | null): void { + if (this.config.seriesList.interval !== interval) { + this.config.seriesList.interval = interval; + this.saveConfig(true); + } + } + + protected setOffset(offset: number): void { + if (this.config.seriesList.offset !== offset) { + this.config.seriesList.offset = offset; + this.saveConfig(true); + } + } + + private fetchList() { + if (this.config.seriesList.interval === null) { + this.seriesService.findAll(list => { + this.seriesUnfiltered = list; + this.applySeriesFilter(); + }); + } else { + this.seriesService.allSeriesPoint(this.config.seriesList.interval, this.config.seriesList.offset, response => { + this.aggregateResponse = response; + this.aggregateUnfiltered = response.seriesPoints; + this.applyAggregateFilter(); + }); + } + } + + private applySeriesFilter() { + this.seriesFiltered = this.seriesUnfiltered.filter(this.filter).sort(this.config.seriesList.sorter.compare); + } + + private applyAggregateFilter() { + this.aggregateUnfiltered = this.aggregateResponse?.seriesPoints || []; + this.aggregateFiltered = this.aggregateUnfiltered.filter(a => this.filter(a.series)).sort((a, b) => this.config.seriesList.sorter.compare(a.series, b.series)); + } + + protected readonly Varying = Varying; +} diff --git a/src/main/angular/src/app/series/series.service.ts b/src/main/angular/src/app/series/series.service.ts index 8adac91..fe9bf54 100644 --- a/src/main/angular/src/app/series/series.service.ts +++ b/src/main/angular/src/app/series/series.service.ts @@ -2,6 +2,8 @@ import {Injectable} from '@angular/core'; import {ApiService, EntityListService, Next, validateNumber} from "../COMMON"; import {Series} from './Series'; import {Interval} from './Interval'; +import {AllSeriesPointResponse} from './AllSeriesPointResponse'; +import {AllSeriesPointRequest} from './AllSeriesPointRequest'; @Injectable({ providedIn: 'root', @@ -14,18 +16,19 @@ export class SeriesService extends EntityListService { super(api, ['Series'], Series.fromJson, Series.equals, Series.compareName); } - points(series: Series, interval: Interval, offset: number, duration: number, next: Next): void { + oneSeriesPoints(series: Series, interval: Interval, offset: number, duration: number, next: Next): void { const request = { id: series.id, interval: interval.name, offset: offset, duration: duration, }; - this.api.postList([...this.path, 'points'], request, (outer: any[]) => outer.map(validateNumber), next); + this.api.postList([...this.path, 'oneSeriesPoints'], request, (outer: any[]) => outer.map(validateNumber), next); } - getById(id: number, next: Next) { - this.getSingle([id], next); + allSeriesPoint(interval: Interval, offset: number, next: (response: AllSeriesPointResponse) => void) { + const request = new AllSeriesPointRequest(interval, offset); + this.api.postSingle([...this.path, 'allSeriesPoint'], request.toJson(), AllSeriesPointResponse.fromJson, next); } } diff --git a/src/main/angular/src/app/series/varying/Varying.ts b/src/main/angular/src/app/series/varying/Varying.ts new file mode 100644 index 0000000..2125b8d --- /dev/null +++ b/src/main/angular/src/app/series/varying/Varying.ts @@ -0,0 +1,26 @@ +import {Series} from "../Series"; +import {validateDate, validateNumber} from "../../COMMON"; + +export class Varying { + + constructor( + readonly series: Series, + readonly date: Date, + readonly min: number, + readonly max: number, + readonly avg: number, + ) { + // + } + + static fromJson(json: any): Varying { + return new Varying( + Series.fromJson(json.series), + validateDate(json.date), + validateNumber(json.min), + validateNumber(json.max), + validateNumber(json.avg), + ); + } + +} diff --git a/src/main/angular/src/app/series/varying/varying-service.ts b/src/main/angular/src/app/series/varying/varying-service.ts index 6cae71f..4f9f4bc 100644 --- a/src/main/angular/src/app/series/varying/varying-service.ts +++ b/src/main/angular/src/app/series/varying/varying-service.ts @@ -1,30 +1,6 @@ import {Injectable} from '@angular/core'; -import {ApiService, CrudService, validateDate, validateNumber} from '../../COMMON'; -import {Series} from '../Series'; - -export class Varying { - - constructor( - readonly series: Series, - readonly date: Date, - readonly min: number, - readonly max: number, - readonly avg: number, - ) { - // - } - - static fromJson(json: any): Varying { - return new Varying( - Series.fromJson(json.series), - validateDate(json.date), - validateNumber(json.min), - validateNumber(json.max), - validateNumber(json.avg), - ); - } - -} +import {ApiService, CrudService} from '../../COMMON'; +import {Varying} from './Varying'; @Injectable({ providedIn: 'root' diff --git a/src/main/angular/src/app/topic/TimestampType.ts b/src/main/angular/src/app/topic/TimestampType.ts new file mode 100644 index 0000000..d6bdf25 --- /dev/null +++ b/src/main/angular/src/app/topic/TimestampType.ts @@ -0,0 +1,4 @@ +export enum TimestampType { + EPOCH_MILLISECONDS = "EPOCH_MILLISECONDS", + EPOCH_SECONDS = "EPOCH_SECONDS", +} diff --git a/src/main/angular/src/app/topic/Topic.ts b/src/main/angular/src/app/topic/Topic.ts new file mode 100644 index 0000000..b68f2c8 --- /dev/null +++ b/src/main/angular/src/app/topic/Topic.ts @@ -0,0 +1,44 @@ +import {TopicQuery} from "./TopicQuery"; + +import {TimestampType} from './TimestampType'; +import {mapNotNull, validateBoolean, validateDate, validateList, validateNumber, validateString} from '../COMMON'; + +export class Topic { + + readonly used: boolean; + + constructor( + readonly id: number, + readonly name: string, + readonly first: Date, + readonly last: Date, + readonly count: number, + readonly enabled: boolean, + readonly timestampType: TimestampType, + readonly timestampQuery: string, + readonly timestampLast: Date | null, + readonly queries: TopicQuery[], + readonly payload: string, + readonly error: string, + ) { + this.used = enabled && timestampQuery.length > 0 && queries.some(q => q.series && q.valueQuery); + } + + static fromJson(json: any): Topic { + return new Topic( + validateNumber(json.id), + validateString(json.name), + validateDate(json.first), + validateDate(json.last), + validateNumber(json.count), + validateBoolean(json.enabled), + validateString(json.timestampType) as TimestampType, + validateString(json.timestampQuery), + mapNotNull(json.timestampLast, validateDate), + validateList(json.queries, TopicQuery.fromJson), + validateString(json.payload), + validateString(json.error), + ); + } + +} diff --git a/src/main/angular/src/app/topic/TopicQuery.ts b/src/main/angular/src/app/topic/TopicQuery.ts new file mode 100644 index 0000000..17f860a --- /dev/null +++ b/src/main/angular/src/app/topic/TopicQuery.ts @@ -0,0 +1,28 @@ +import {TopicQueryFunction} from './TopicQueryFunction'; +import {Series} from '../series/Series'; +import {validateString} from '../COMMON'; + +export class TopicQuery { + // series + // valueQuery + // beginQuery + // terminatedQuery + // function + // factor + constructor( + readonly series: Series, + readonly valueQuery: string, + readonly func: TopicQueryFunction, + ) { + // + } + + static fromJson(json: any): TopicQuery { + return new TopicQuery( + Series.fromJson(json.series), + validateString(json.valueQuery), + validateString(json.function) as TopicQueryFunction, + ); + } + +} diff --git a/src/main/angular/src/app/topic/TopicQueryFunction.ts b/src/main/angular/src/app/topic/TopicQueryFunction.ts new file mode 100644 index 0000000..cb1ad8d --- /dev/null +++ b/src/main/angular/src/app/topic/TopicQueryFunction.ts @@ -0,0 +1,5 @@ +export enum TopicQueryFunction { + NONE = "NONE", + ONLY_POSITIVE = "ONLY_POSITIVE", + ONLY_NEGATIVE_BUT_NEGATE = "ONLY_NEGATIVE_BUT_NEGATE", +} diff --git a/src/main/angular/src/app/topic/list/topic-list.component.html b/src/main/angular/src/app/topic/list/topic-list.component.html new file mode 100644 index 0000000..5fcebc2 --- /dev/null +++ b/src/main/angular/src/app/topic/list/topic-list.component.html @@ -0,0 +1,33 @@ + + @for (topic of sorted(); track topic.id) { + + + + + + + + + + + @for (query of topic.queries; track $index) { + + + + + + @if (query.series.value.showDot) { + + } @else { + + } + + + + + } + + + + } +
{{ topic.name }}{{ topic.last | date:'long':'':'de-DE' }}{{ topic.payload }}
{{ topic.timestampQuery }}{{ topic.timestampType }}{{ topic.timestampLast | date:'long':'':'de-DE' }}
{{ query.valueQuery }}{{ query.func }}{{ query.series.name }}{{ query.series.value.integer }},{{ query.series.value.fraction }}{{ query.series.unit }}{{ query.series.date | date:'long':'':'de-DE' }}
 
diff --git a/src/main/angular/src/app/topic/list/topic-list.component.less b/src/main/angular/src/app/topic/list/topic-list.component.less new file mode 100644 index 0000000..fe6ebcb --- /dev/null +++ b/src/main/angular/src/app/topic/list/topic-list.component.less @@ -0,0 +1,104 @@ +@import "../../../config"; + +table.TopicList { + + tr { + th, td { + border: 1px solid gray; + white-space: nowrap; + vertical-align: top; + } + } + + tr.head { + background-color: lightgray; + } + + tr.query { + background-color: lightskyblue; + } + + tr.timestamp { + background-color: darkseagreen; + } + + tr.spacer { + th, td { + border: none; + } + } + + .empty { + background-color: #ffd3d3; + } + + .name { + font-weight: bold; + } + + .last { + + } + + .payload { + white-space: unset !important; + } + + .timestampType { + + } + + .timestampQuery { + + } + + .timestampLast { + + } + + .valueQuery { + + } + + .valueFunction { + + } + + .valueSeries { + text-align: right; + } + + .valueSeriesValueInteger { + border-right: none !important; + padding-right: 0; + text-align: right; + width: 0; + } + + .valueSeriesValueDot { + border-right: none !important; + padding-right: 0; + border-left: none !important; + padding-left: 0; + width: 0; + } + + .valueSeriesValueFraction { + border-right: none !important; + padding-right: 0; + border-left: none !important; + padding-left: 0; + width: 0; + } + + .valueSeriesValueUnit { + border-left: none !important; + text-align: left; + width: 0; + } + + .valueSeriesDate { + width: 0; + } + +} diff --git a/src/main/angular/src/app/topic/list/topic-list.component.ts b/src/main/angular/src/app/topic/list/topic-list.component.ts new file mode 100644 index 0000000..d7f2035 --- /dev/null +++ b/src/main/angular/src/app/topic/list/topic-list.component.ts @@ -0,0 +1,91 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {DatePipe} from '@angular/common'; +import {Topic} from '../Topic'; +import {TopicService} from '../topic.service'; +import {FormsModule} from '@angular/forms'; +import {Subscription, timer} from 'rxjs'; +import {TimestampType} from '../TimestampType'; +import {TopicQueryFunction} from '../TopicQueryFunction'; +import {Series} from '../../series/Series'; +import {SeriesService} from '../../series/series.service'; +import {Config} from '../../config/Config'; +import {ageString} from '../../COMMON'; +import {ConfigService} from '../../config/config.service'; + +@Component({ + selector: 'app-topic-list', + imports: [ + FormsModule, + DatePipe + ], + templateUrl: './topic-list.component.html', + styleUrl: './topic-list.component.less' +}) +export class TopicListComponent implements OnInit, OnDestroy { + + protected readonly ageString = ageString; + + protected now: Date = new Date(); + + protected topicList: Topic[] = []; + + protected seriesList: Series[] = []; + + protected config: Config = new Config(); + + private readonly subs: Subscription[] = []; + + constructor( + readonly configService: ConfigService, + readonly seriesService: SeriesService, + readonly topicService: TopicService, + ) { + // + } + + ngOnInit(): void { + this.configService.get(config => this.config = config); + this.seriesService.findAll(list => this.seriesList = list.sort((a, b) => a.name.localeCompare(b.name))); + this.topicService.findAll(list => this.topicList = list); + this.subs.push(this.topicService.subscribe(this.update)); + this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date())); + } + + ngOnDestroy(): void { + this.subs.forEach(sub => sub.unsubscribe()); + } + + sorted() { + return this.topicList.filter(this.filter).sort(this.compare); + } + + private readonly update = (topic: Topic) => { + const index = this.topicList.findIndex(t => t.id === topic.id); + if (index >= 0) { + this.topicList.splice(index, 1, topic); + } else { + this.topicList.push(topic); + } + }; + + private readonly filter = (topic: Topic) => { + return ((!topic.used && this.config.topicList.unused) || (topic.used && this.config.topicList.used)); + } + + private readonly compare = (a: Topic, b: Topic) => { + return a.name.localeCompare(b.name); + } + + TimestampTypes() { + return Object.values(TimestampType); + } + + TopicQueryFunctions() { + return Object.values(TopicQueryFunction); + } + + configSet() { + this.configService.set(this.config, config => this.config = config); + } + +} diff --git a/src/main/angular/src/app/topic/topic.service.ts b/src/main/angular/src/app/topic/topic.service.ts new file mode 100644 index 0000000..8ac6d60 --- /dev/null +++ b/src/main/angular/src/app/topic/topic.service.ts @@ -0,0 +1,16 @@ +import {Injectable} from '@angular/core'; +import {Topic} from './Topic'; +import {ApiService, CrudService} from '../COMMON'; + +@Injectable({ + providedIn: 'root' +}) +export class TopicService extends CrudService { + + constructor( + api: ApiService, + ) { + super(api, ["Topic"], Topic.fromJson); + } + +} diff --git a/src/main/angular/src/config.less b/src/main/angular/src/config.less new file mode 100644 index 0000000..ff86788 --- /dev/null +++ b/src/main/angular/src/config.less @@ -0,0 +1,2 @@ +@space: 0.25em; +@border: 0.01em; diff --git a/src/main/java/de/ph87/data/config/Config.java b/src/main/java/de/ph87/data/config/Config.java new file mode 100644 index 0000000..3241577 --- /dev/null +++ b/src/main/java/de/ph87/data/config/Config.java @@ -0,0 +1,85 @@ +package de.ph87.data.config; + +import de.ph87.data.series.data.Interval; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OrderColumn; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@ToString +@NoArgsConstructor +public class Config { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(nullable = false) + private boolean hidden = true; + + @Column(nullable = false) + private boolean used = true; + + @Column(nullable = false) + private boolean unused = true; + + @Column(nullable = false) + private boolean ok = true; + + @Column(nullable = false) + private boolean error = true; + + @Column(nullable = false) + private boolean details = true; + + @Nullable + @Column(name = "`interval`") + @Enumerated(EnumType.STRING) + private Interval interval = null; + + @Column(nullable = false, name = "`offset`") + private long offset = 0; + + @NonNull + @Column(nullable = false) + private String search = ""; + + @NonNull + @OrderColumn(name = "index") + @ElementCollection(fetch = FetchType.EAGER) + private List seriesListOrders = new ArrayList<>(List.of(new Order("name", Direction.ASC))); + + public Config(@NonNull final ConfigDto dto) { + set(dto); + } + + public void set(@NonNull final ConfigDto dto) { + this.hidden = dto.topicList.isHidden(); + this.used = dto.topicList.isUsed(); + this.unused = dto.topicList.isUnused(); + this.ok = dto.topicList.isOk(); + this.error = dto.topicList.isError(); + this.details = dto.topicList.isDetails(); + this.interval = dto.seriesList.getInterval(); + this.offset = dto.seriesList.getOffset(); + this.search = dto.seriesList.getSearch(); + this.seriesListOrders = dto.getSeriesList().orders.stream().map(Order::new).toList(); + } + +} diff --git a/src/main/java/de/ph87/data/config/ConfigController.java b/src/main/java/de/ph87/data/config/ConfigController.java new file mode 100644 index 0000000..8ac13a3 --- /dev/null +++ b/src/main/java/de/ph87/data/config/ConfigController.java @@ -0,0 +1,30 @@ +package de.ph87.data.config; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@CrossOrigin +@RestController +@RequestMapping("Config") +@RequiredArgsConstructor +public class ConfigController { + + private final ConfigService configService; + + @GetMapping("get") + public ConfigDto get() { + return configService.get(); + } + + @PostMapping("set") + public ConfigDto set(@RequestBody @NonNull final ConfigDto inbound) { + return configService.set(inbound); + } + +} diff --git a/src/main/java/de/ph87/data/config/ConfigDto.java b/src/main/java/de/ph87/data/config/ConfigDto.java new file mode 100644 index 0000000..5ff72ea --- /dev/null +++ b/src/main/java/de/ph87/data/config/ConfigDto.java @@ -0,0 +1,28 @@ +package de.ph87.data.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NonNull; + +@Data +public class ConfigDto { + + public final ConfigTopicListDto topicList; + + @NonNull + public final ConfigSeriesListDto seriesList; + + public ConfigDto( + @JsonProperty("topicList") @NonNull final ConfigTopicListDto topicList, + @JsonProperty("seriesList") @NonNull final ConfigSeriesListDto seriesList + ) { + this.topicList = topicList; + this.seriesList = seriesList; + } + + ConfigDto(@NonNull final Config config) { + this.topicList = new ConfigTopicListDto(config); + this.seriesList = ConfigSeriesListDto.fromDB(config); + } + +} diff --git a/src/main/java/de/ph87/data/config/ConfigRepository.java b/src/main/java/de/ph87/data/config/ConfigRepository.java new file mode 100644 index 0000000..e401f9e --- /dev/null +++ b/src/main/java/de/ph87/data/config/ConfigRepository.java @@ -0,0 +1,11 @@ +package de.ph87.data.config; + +import org.springframework.data.repository.ListCrudRepository; + +import java.util.Optional; + +public interface ConfigRepository extends ListCrudRepository { + + Optional findFirstBy(); + +} diff --git a/src/main/java/de/ph87/data/config/ConfigSeriesListDto.java b/src/main/java/de/ph87/data/config/ConfigSeriesListDto.java new file mode 100644 index 0000000..ce23130 --- /dev/null +++ b/src/main/java/de/ph87/data/config/ConfigSeriesListDto.java @@ -0,0 +1,52 @@ +package de.ph87.data.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.data.series.data.Interval; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.Data; +import lombok.NonNull; + +import java.util.List; + +@Data +public class ConfigSeriesListDto { + + @Column + @Nullable + @Enumerated(EnumType.STRING) + public final Interval interval; + + @Column(nullable = false) + public final long offset; + + @NonNull + public final String search; + + public final List orders; + + public ConfigSeriesListDto( + @JsonProperty("interval") @Nullable final Interval interval, + @JsonProperty("offset") final long offset, + @JsonProperty("search") @NonNull final String search, + @JsonProperty("orders") @NonNull final List orders + ) { + this.interval = interval; + this.offset = offset; + this.search = search; + this.orders = orders; + } + + @NonNull + public static ConfigSeriesListDto fromDB(@NonNull final Config orders) { + return new ConfigSeriesListDto( + orders.getInterval(), + orders.getOffset(), + orders.getSearch(), + orders.getSeriesListOrders().stream().map(OrderDto::new).toList() + ); + } + +} diff --git a/src/main/java/de/ph87/data/config/ConfigService.java b/src/main/java/de/ph87/data/config/ConfigService.java new file mode 100644 index 0000000..f751471 --- /dev/null +++ b/src/main/java/de/ph87/data/config/ConfigService.java @@ -0,0 +1,26 @@ +package de.ph87.data.config; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ConfigService { + + private final ConfigRepository configRepository; + + @Transactional + public ConfigDto get() { + return new ConfigDto(configRepository.findFirstBy().orElseGet(() -> configRepository.save(new Config()))); + } + + @Transactional + public ConfigDto set(@NonNull final ConfigDto inbound) { + return new ConfigDto(configRepository.findFirstBy().stream().peek(old -> old.set(inbound)).findFirst().orElseGet(() -> configRepository.save(new Config(inbound)))); + } + +} diff --git a/src/main/java/de/ph87/data/config/ConfigTopicListDto.java b/src/main/java/de/ph87/data/config/ConfigTopicListDto.java new file mode 100644 index 0000000..550ddd1 --- /dev/null +++ b/src/main/java/de/ph87/data/config/ConfigTopicListDto.java @@ -0,0 +1,47 @@ +package de.ph87.data.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NonNull; + +@Data +public class ConfigTopicListDto { + + public final boolean hidden; + + public final boolean used; + + public final boolean unused; + + public final boolean ok; + + public final boolean error; + + public final boolean details; + + public ConfigTopicListDto( + @JsonProperty("hidden") final boolean hidden, + @JsonProperty("used") final boolean used, + @JsonProperty("unused") final boolean unused, + @JsonProperty("ok") final boolean ok, + @JsonProperty("error") final boolean error, + @JsonProperty("details") final boolean details + ) { + this.hidden = hidden; + this.used = used; + this.unused = unused; + this.ok = ok; + this.error = error; + this.details = details; + } + + ConfigTopicListDto(@NonNull final Config config) { + this.hidden = config.isHidden(); + this.used = config.isUsed(); + this.unused = config.isUnused(); + this.ok = config.isOk(); + this.error = config.isError(); + this.details = config.isDetails(); + } + +} diff --git a/src/main/java/de/ph87/data/config/Direction.java b/src/main/java/de/ph87/data/config/Direction.java new file mode 100644 index 0000000..4463204 --- /dev/null +++ b/src/main/java/de/ph87/data/config/Direction.java @@ -0,0 +1,5 @@ +package de.ph87.data.config; + +public enum Direction { + ASC, DESC +} diff --git a/src/main/java/de/ph87/data/config/Order.java b/src/main/java/de/ph87/data/config/Order.java new file mode 100644 index 0000000..c88ceb7 --- /dev/null +++ b/src/main/java/de/ph87/data/config/Order.java @@ -0,0 +1,37 @@ +package de.ph87.data.config; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString +@Embeddable +@NoArgsConstructor +public class Order { + + @NonNull + @Column(nullable = false) + private String property; + + @NonNull + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Direction direction; + + public Order(@NonNull final OrderDto dto) { + this.property = dto.property; + this.direction = dto.direction; + } + + public Order(@NonNull final String property, @NonNull final Direction direction) { + this.property = property; + this.direction = direction; + } + +} diff --git a/src/main/java/de/ph87/data/config/OrderDto.java b/src/main/java/de/ph87/data/config/OrderDto.java new file mode 100644 index 0000000..f757677 --- /dev/null +++ b/src/main/java/de/ph87/data/config/OrderDto.java @@ -0,0 +1,29 @@ +package de.ph87.data.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NonNull; + +@Data +public class OrderDto { + + @NonNull + public final String property; + + @NonNull + public final Direction direction; + + public OrderDto( + @JsonProperty("property") @NonNull final String property, + @JsonProperty("direction") @NonNull final Direction direction + ) { + this.property = property; + this.direction = direction; + } + + public OrderDto(@NonNull final Order order) { + this.property = order.getProperty(); + this.direction = order.getDirection(); + } + +} diff --git a/src/main/java/de/ph87/data/series/AllSeriesPointRequest.java b/src/main/java/de/ph87/data/series/AllSeriesPointRequest.java new file mode 100644 index 0000000..f6ed669 --- /dev/null +++ b/src/main/java/de/ph87/data/series/AllSeriesPointRequest.java @@ -0,0 +1,34 @@ +package de.ph87.data.series; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.ph87.data.series.data.Interval; +import lombok.Data; +import lombok.NonNull; + +import java.time.ZonedDateTime; + +@Data +public class AllSeriesPointRequest implements ISeriesPointRequest { + + @NonNull + public final Interval interval; + + public final long offset; + + @NonNull + public final ZonedDateTime first; + + @NonNull + public final ZonedDateTime after; + + public AllSeriesPointRequest( + @JsonProperty("interval") final Interval interval, + @JsonProperty("offset") final long offset + ) { + this.interval = interval; + this.offset = offset; + this.first = interval.align.apply(ZonedDateTime.now()).minus(interval.amount * offset, interval.unit); + this.after = this.first.plus(interval.amount, interval.unit); + } + +} diff --git a/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java b/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java new file mode 100644 index 0000000..51ace3a --- /dev/null +++ b/src/main/java/de/ph87/data/series/AllSeriesPointResponse.java @@ -0,0 +1,29 @@ +package de.ph87.data.series; + +import jakarta.annotation.Nullable; +import lombok.Data; +import lombok.NonNull; + +import java.util.List; + +@Data +public class AllSeriesPointResponse { + + @NonNull + public final AllSeriesPointRequest request; + + @NonNull + public final List seriesPoints; + + @Data + public static class Entry { + + @NonNull + public final SeriesDto series; + + @Nullable + public final SeriesPoint point; + + } + +} diff --git a/src/main/java/de/ph87/data/series/ISeriesPointRequest.java b/src/main/java/de/ph87/data/series/ISeriesPointRequest.java new file mode 100644 index 0000000..5a884f0 --- /dev/null +++ b/src/main/java/de/ph87/data/series/ISeriesPointRequest.java @@ -0,0 +1,19 @@ +package de.ph87.data.series; + +import de.ph87.data.series.data.Interval; +import lombok.NonNull; + +import java.time.ZonedDateTime; + +public interface ISeriesPointRequest { + + @NonNull + Interval getInterval(); + + @NonNull + ZonedDateTime getFirst(); + + @NonNull + ZonedDateTime getAfter(); + +} diff --git a/src/main/java/de/ph87/data/series/SeriesPointsRequest.java b/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java similarity index 90% rename from src/main/java/de/ph87/data/series/SeriesPointsRequest.java rename to src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java index 1d349f3..134ae6c 100644 --- a/src/main/java/de/ph87/data/series/SeriesPointsRequest.java +++ b/src/main/java/de/ph87/data/series/OneSeriesPointsRequest.java @@ -9,7 +9,7 @@ import lombok.NonNull; import java.time.ZonedDateTime; @Data -public class SeriesPointsRequest { +public class OneSeriesPointsRequest implements ISeriesPointRequest { public final long id; @@ -28,7 +28,7 @@ public class SeriesPointsRequest { @JsonIgnore public final ZonedDateTime after; - public SeriesPointsRequest( + public OneSeriesPointsRequest( @JsonProperty("id") final long id, @JsonProperty("interval") final Interval interval, @JsonProperty("offset") final long offset, diff --git a/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java b/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java new file mode 100644 index 0000000..ddeecc5 --- /dev/null +++ b/src/main/java/de/ph87/data/series/OneSeriesPointsResponse.java @@ -0,0 +1,14 @@ +package de.ph87.data.series; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.Data; + +import java.util.List; + +@Data +@JsonSerialize(using = OneSeriesPointsResponseSerializer.class) +public class OneSeriesPointsResponse { + + public final List points; + +} diff --git a/src/main/java/de/ph87/data/series/OneSeriesPointsResponseSerializer.java b/src/main/java/de/ph87/data/series/OneSeriesPointsResponseSerializer.java new file mode 100644 index 0000000..d71d147 --- /dev/null +++ b/src/main/java/de/ph87/data/series/OneSeriesPointsResponseSerializer.java @@ -0,0 +1,22 @@ +package de.ph87.data.series; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; + +public class OneSeriesPointsResponseSerializer extends JsonSerializer { + + @Override + public void serialize(final OneSeriesPointsResponse result, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartArray(); + for (final SeriesPoint point : result.points) { + jsonGenerator.writeStartArray(); + point.toJson(jsonGenerator); + jsonGenerator.writeEndArray(); + } + jsonGenerator.writeEndArray(); + } + +} diff --git a/src/main/java/de/ph87/data/series/SeriesController.java b/src/main/java/de/ph87/data/series/SeriesController.java index 28f9183..b2bc5e2 100644 --- a/src/main/java/de/ph87/data/series/SeriesController.java +++ b/src/main/java/de/ph87/data/series/SeriesController.java @@ -32,9 +32,15 @@ public class SeriesController { return seriesRepository.getDtoById(id); } - @PostMapping("points") - public List points(@NonNull @RequestBody final SeriesPointsRequest request) { - return SeriesService.points(request); + @NonNull + @PostMapping("oneSeriesPoints") + public OneSeriesPointsResponse oneSeriesPoints(@NonNull @RequestBody final OneSeriesPointsRequest request) { + return SeriesService.oneSeriesPoints(request); + } + + @PostMapping("allSeriesPoint") + public AllSeriesPointResponse allSeriesPoint(@NonNull @RequestBody final AllSeriesPointRequest request) { + return SeriesService.allSeriesPoint(request); } } diff --git a/src/main/java/de/ph87/data/series/SeriesPoint.java b/src/main/java/de/ph87/data/series/SeriesPoint.java index 94fc92e..31fdf65 100644 --- a/src/main/java/de/ph87/data/series/SeriesPoint.java +++ b/src/main/java/de/ph87/data/series/SeriesPoint.java @@ -1,11 +1,9 @@ package de.ph87.data.series; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.io.IOException; -@JsonSerialize(using = SeriesPointSerializer.class) public interface SeriesPoint { void toJson(final JsonGenerator jsonGenerator) throws IOException; diff --git a/src/main/java/de/ph87/data/series/SeriesPointSerializer.java b/src/main/java/de/ph87/data/series/SeriesPointSerializer.java deleted file mode 100644 index b4e0e57..0000000 --- a/src/main/java/de/ph87/data/series/SeriesPointSerializer.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.ph87.data.series; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - -import java.io.IOException; - -public class SeriesPointSerializer extends JsonSerializer { - - @Override - public void serialize(final SeriesPoint seriesPoint, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException { - jsonGenerator.writeStartArray(); - seriesPoint.toJson(jsonGenerator); - jsonGenerator.writeEndArray(); - } - -} diff --git a/src/main/java/de/ph87/data/series/SeriesService.java b/src/main/java/de/ph87/data/series/SeriesService.java index 01545a5..e09b267 100644 --- a/src/main/java/de/ph87/data/series/SeriesService.java +++ b/src/main/java/de/ph87/data/series/SeriesService.java @@ -26,8 +26,27 @@ public class SeriesService { private final VaryingService varyingService; @NonNull - public List points(@NonNull final SeriesPointsRequest request) { + public OneSeriesPointsResponse oneSeriesPoints(@NonNull final OneSeriesPointsRequest request) { final Series series = seriesRepository.findById(request.id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + return new OneSeriesPointsResponse(getSeriesPoints(series, request)); + } + + @NonNull + public AllSeriesPointResponse allSeriesPoint(@NonNull final AllSeriesPointRequest request) { + final List seriesPoints = seriesRepository.findAll().stream().map(series -> map(series, request)).toList(); + return new AllSeriesPointResponse(request, seriesPoints); + } + + @NonNull + private AllSeriesPointResponse.Entry map(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { + final List points = getSeriesPoints(series, request); + final SeriesDto seriesDto = new SeriesDto(series, false); + final SeriesPoint point = points.isEmpty() ? null : points.getFirst(); + return new AllSeriesPointResponse.Entry(seriesDto, point); + } + + @NonNull + private List getSeriesPoints(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { return switch (series.getType()) { case BOOL -> boolService.points(series, request); case DELTA -> deltaService.points(series, request); diff --git a/src/main/java/de/ph87/data/series/data/bool/BoolService.java b/src/main/java/de/ph87/data/series/data/bool/BoolService.java index c88124e..afcd87b 100644 --- a/src/main/java/de/ph87/data/series/data/bool/BoolService.java +++ b/src/main/java/de/ph87/data/series/data/bool/BoolService.java @@ -1,7 +1,7 @@ package de.ph87.data.series.data.bool; +import de.ph87.data.series.ISeriesPointRequest; import de.ph87.data.series.Series; -import de.ph87.data.series.SeriesPointsRequest; import de.ph87.data.series.data.DataId; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -59,8 +59,8 @@ public class BoolService { } @NonNull - public List points(@NonNull final Series series, @NonNull final SeriesPointsRequest request) { - return boolRepo.points(series, request.first, request.after); + public List points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { + return boolRepo.points(series, request.getFirst(), request.getAfter()); } } diff --git a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java index e621bf4..7f06802 100644 --- a/src/main/java/de/ph87/data/series/data/delta/DeltaService.java +++ b/src/main/java/de/ph87/data/series/data/delta/DeltaService.java @@ -1,7 +1,7 @@ package de.ph87.data.series.data.delta; +import de.ph87.data.series.ISeriesPointRequest; import de.ph87.data.series.Series; -import de.ph87.data.series.SeriesPointsRequest; import de.ph87.data.series.data.DataId; import de.ph87.data.series.data.Interval; import lombok.NonNull; @@ -52,14 +52,14 @@ public class DeltaService { } @NonNull - public List points(@NonNull final Series series, @NonNull final SeriesPointsRequest request) { - return switch (request.interval) { - case FIVE -> five.points(series, request.first, request.after); - case HOUR -> hour.points(series, request.first, request.after); - case DAY -> day.points(series, request.first, request.after); - case WEEK -> week.points(series, request.first, request.after); - case MONTH -> month.points(series, request.first, request.after); - case YEAR -> year.points(series, request.first, request.after); + public List points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { + return switch (request.getInterval()) { + case FIVE -> five.points(series, request.getFirst(), request.getAfter()); + case HOUR -> hour.points(series, request.getFirst(), request.getAfter()); + case DAY -> day.points(series, request.getFirst(), request.getAfter()); + case WEEK -> week.points(series, request.getFirst(), request.getAfter()); + case MONTH -> month.points(series, request.getFirst(), request.getAfter()); + case YEAR -> year.points(series, request.getFirst(), request.getAfter()); }; } diff --git a/src/main/java/de/ph87/data/series/data/varying/VaryingService.java b/src/main/java/de/ph87/data/series/data/varying/VaryingService.java index 46c48b5..0d92874 100644 --- a/src/main/java/de/ph87/data/series/data/varying/VaryingService.java +++ b/src/main/java/de/ph87/data/series/data/varying/VaryingService.java @@ -1,7 +1,7 @@ package de.ph87.data.series.data.varying; +import de.ph87.data.series.ISeriesPointRequest; import de.ph87.data.series.Series; -import de.ph87.data.series.SeriesPointsRequest; import de.ph87.data.series.data.DataId; import de.ph87.data.series.data.Interval; import lombok.NonNull; @@ -52,14 +52,14 @@ public class VaryingService { } @NonNull - public List points(@NonNull final Series series, @NonNull final SeriesPointsRequest request) { - return switch (request.interval) { - case FIVE -> five.points(series, request.first, request.after); - case HOUR -> hour.points(series, request.first, request.after); - case DAY -> day.points(series, request.first, request.after); - case WEEK -> week.points(series, request.first, request.after); - case MONTH -> month.points(series, request.first, request.after); - case YEAR -> year.points(series, request.first, request.after); + public List points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) { + return switch (request.getInterval()) { + case FIVE -> five.points(series, request.getFirst(), request.getAfter()); + case HOUR -> hour.points(series, request.getFirst(), request.getAfter()); + case DAY -> day.points(series, request.getFirst(), request.getAfter()); + case WEEK -> week.points(series, request.getFirst(), request.getAfter()); + case MONTH -> month.points(series, request.getFirst(), request.getAfter()); + case YEAR -> year.points(series, request.getFirst(), request.getAfter()); }; } diff --git a/src/main/java/de/ph87/data/topic/Topic.java b/src/main/java/de/ph87/data/topic/Topic.java index 21efd48..4ea42db 100644 --- a/src/main/java/de/ph87/data/topic/Topic.java +++ b/src/main/java/de/ph87/data/topic/Topic.java @@ -2,6 +2,7 @@ package de.ph87.data.topic; import de.ph87.data.log.AbstractEntityLog; import de.ph87.data.topic.query.TopicQuery; +import jakarta.annotation.Nullable; import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; @@ -11,6 +12,7 @@ import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Lob; import jakarta.persistence.Version; import lombok.Getter; import lombok.NoArgsConstructor; @@ -60,6 +62,11 @@ public class Topic extends AbstractEntityLog { @Column(nullable = false) private TimestampType timestampType = TimestampType.EPOCH_SECONDS; + @Setter + @Column + @Nullable + private ZonedDateTime timestampLast = null; + @Setter @NonNull @Column(nullable = false) @@ -70,6 +77,19 @@ public class Topic extends AbstractEntityLog { @ElementCollection(fetch = FetchType.EAGER) private List queries = new ArrayList<>(); + @Lob + @Setter + @NonNull + @ToString.Exclude + @Column(nullable = false) + private String error = ""; + + @Lob + @NonNull + @ToString.Exclude + @Column(nullable = false) + private String payload = ""; + public Topic(@NonNull final String name) { this.name = name; this.first = ZonedDateTime.now(); @@ -77,8 +97,9 @@ public class Topic extends AbstractEntityLog { this.count = 1; } - public void update() { + public void update(@NonNull final String payload) { this.last = ZonedDateTime.now(); + this.payload = payload; this.count++; } diff --git a/src/main/java/de/ph87/data/topic/TopicDto.java b/src/main/java/de/ph87/data/topic/TopicDto.java index 0e45cdd..1a52f8b 100644 --- a/src/main/java/de/ph87/data/topic/TopicDto.java +++ b/src/main/java/de/ph87/data/topic/TopicDto.java @@ -2,6 +2,7 @@ package de.ph87.data.topic; import de.ph87.data.topic.query.TopicQueryDto; import de.ph87.data.websocket.IWebsocketMessage; +import jakarta.annotation.Nullable; import lombok.Data; import lombok.NonNull; @@ -32,9 +33,18 @@ public class TopicDto implements IWebsocketMessage { @NonNull public final String timestampQuery; + @Nullable + public final ZonedDateTime timestampLast; + @NonNull public final List queries; + @NonNull + public final String error; + + @NonNull + public final String payload; + public TopicDto(@NonNull final Topic topic) { this.id = topic.getId(); this.name = topic.getName(); @@ -44,7 +54,10 @@ public class TopicDto implements IWebsocketMessage { this.enabled = topic.isEnabled(); this.timestampType = topic.getTimestampType(); this.timestampQuery = topic.getTimestampQuery(); + this.timestampLast = topic.getTimestampLast(); this.queries = topic.getQueries().stream().map(TopicQueryDto::new).toList(); + this.error = topic.getError(); + this.payload = topic.getPayload(); } } diff --git a/src/main/java/de/ph87/data/topic/TopicReceiver.java b/src/main/java/de/ph87/data/topic/TopicReceiver.java index 887b1cf..6b9e5d0 100644 --- a/src/main/java/de/ph87/data/topic/TopicReceiver.java +++ b/src/main/java/de/ph87/data/topic/TopicReceiver.java @@ -37,41 +37,48 @@ public class TopicReceiver { private final ApplicationEventPublisher applicationEventPublisher; + private final TopicService topicService; + @Transactional public void receive(@NonNull final MqttInbound inbound) { - final Topic topic = updateOrCreate(inbound.topic); - if (!topic.isEnabled()) { - log.debug("Topic is not enabled: topic={}", topic); - return; - } - if (topic.getTimestampQuery().isEmpty()) { - log.debug("Topic timestampQuery is not set: topic={}", topic); - return; - } - if (topic.getQueries().isEmpty()) { - log.debug("Topic queries not set: topic={}", topic); - return; - } - - log.debug("Parsing Topic payload: topic={}", topic); - final DocumentContext json; + final Topic topic = updateOrCreate(inbound.topic, inbound.payload); try { - json = JsonPath.parse(inbound.payload); - } catch (Exception e) { - topic.error(log, "Error parsing JSON: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e); - return; - } + if (!topic.isEnabled()) { + log.debug("Topic is not enabled: topic={}", topic); + return; + } + if (topic.getTimestampQuery().isEmpty()) { + log.debug("Topic timestampQuery is not set: topic={}", topic); + return; + } + if (topic.getQueries().isEmpty()) { + log.debug("Topic queries not set: topic={}", topic); + return; + } - log.debug("Executing Topic timestampQuery: topic={}", topic); - final ZonedDateTime date; - try { - date = queryTimestamp(json, topic.getTimestampQuery(), topic.getTimestampType()); - } catch (Exception e) { - topic.error(log, "Error executing Topic timestampQuery: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e); - return; - } + log.debug("Parsing Topic payload: topic={}", topic); + final DocumentContext json; + try { + json = JsonPath.parse(inbound.payload); + } catch (Exception e) { + topic.error(log, "Error parsing JSON: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e); + return; + } - topic.getQueries().forEach(query -> query(topic, inbound, json, date, query)); + log.debug("Executing Topic timestampQuery: topic={}", topic); + final ZonedDateTime date; + try { + date = queryTimestamp(json, topic.getTimestampQuery(), topic.getTimestampType()); + } catch (Exception e) { + topic.error(log, "Error executing Topic timestampQuery: %s\n topic=%s\n inbound=%s".formatted(e.toString(), topic, inbound), e); + return; + } + + topic.setTimestampLast(date); + topic.getQueries().forEach(query -> query(topic, inbound, json, date, query)); + } finally { + topicService.publish(topic); + } } private void query(@NonNull final Topic topic, @NonNull final MqttInbound inbound, @NonNull final DocumentContext json, @NonNull final ZonedDateTime date, @NonNull final TopicQuery query) { @@ -135,8 +142,8 @@ public class TopicReceiver { } @NonNull - private Topic updateOrCreate(@NonNull final String name) { - return topicRepository.findByName(name).stream().peek(Topic::update).findFirst().orElseGet(() -> topicRepository.save(new Topic(name))); + private Topic updateOrCreate(@NonNull final String name, @NonNull final String payload) { + return topicRepository.findByName(name).stream().peek(topic -> topic.update(payload)).findFirst().orElseGet(() -> topicRepository.save(new Topic(name))); } @NonNull diff --git a/src/main/java/de/ph87/data/topic/TopicService.java b/src/main/java/de/ph87/data/topic/TopicService.java index ba2e4ee..8659315 100644 --- a/src/main/java/de/ph87/data/topic/TopicService.java +++ b/src/main/java/de/ph87/data/topic/TopicService.java @@ -38,6 +38,11 @@ public class TopicService { final Topic topic = topicRepository.findById(id).orElseThrow(); modifier.accept(topic); log.info("Topic CHANGED: {}", topic); + return publish(topic); + } + + @NonNull + public TopicDto publish(@NonNull final Topic topic) { final TopicDto dto = new TopicDto(topic); applicationEventPublisher.publishEvent(dto); return dto;