UI: TopicList + SeriesList
This commit is contained in:
parent
6172e888bf
commit
d17255ecc1
@ -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<T> = (json: any) => T;
|
||||
|
||||
@ -288,3 +289,54 @@ export function UTN<T>(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`;
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
<div class="content">
|
||||
<div class="item" [routerLink]="['Dashboard']" routerLinkActive="itemActive" (click)="sidebar = false">Dashboard</div>
|
||||
<div class="item" [routerLink]="['PlotEditor']" routerLinkActive="itemActive" (click)="sidebar = false">Diagramm-Editor</div>
|
||||
<div class="item" [routerLink]="['TopicList']" routerLinkActive="itemActive" (click)="sidebar = false">Topics</div>
|
||||
<div class="item" [routerLink]="['SeriesList']" routerLinkActive="itemActive" (click)="sidebar = false">Messreihen</div>
|
||||
</div>
|
||||
}
|
||||
<div class="handle" (click)="sidebar = !sidebar">
|
||||
|
||||
@ -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'},
|
||||
|
||||
29
src/main/angular/src/app/common/Value.ts
Normal file
29
src/main/angular/src/app/common/Value.ts
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
12
src/main/angular/src/app/common/column/column.component.html
Normal file
12
src/main/angular/src/app/common/column/column.component.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div (click)="click()">
|
||||
{{ title === null ? column.title : title }}
|
||||
@if (sorter.isAscending(column)) {
|
||||
<span class="sub">↑</span>
|
||||
}
|
||||
@if (sorter.isDescending(column)) {
|
||||
<span class="sub">↓</span>
|
||||
}
|
||||
@if (sorter.showCount(column)) {
|
||||
<span class="sub">{{ sorter.indexOf(column) + 1 }}</span>
|
||||
}
|
||||
</div>
|
||||
@ -0,0 +1,3 @@
|
||||
.sub {
|
||||
font-size: 50%;
|
||||
}
|
||||
30
src/main/angular/src/app/common/column/column.component.ts
Normal file
30
src/main/angular/src/app/common/column/column.component.ts
Normal file
@ -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<T> {
|
||||
|
||||
@Input()
|
||||
column!: Column<T>;
|
||||
|
||||
@Input()
|
||||
sorter!: Sorter<T>;
|
||||
|
||||
@Input()
|
||||
title: string | null = null;
|
||||
|
||||
@Output()
|
||||
readonly onChange: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
click() {
|
||||
this.sorter.toggle(this.column);
|
||||
this.onChange.emit();
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/angular/src/app/common/sorter/Column.ts
Normal file
26
src/main/angular/src/app/common/sorter/Column.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {Compare} from '../../COMMON';
|
||||
|
||||
export class Column<T> {
|
||||
|
||||
private constructor(
|
||||
readonly name: string,
|
||||
readonly title: string,
|
||||
readonly compare: Compare<T>,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static create<T, R>(
|
||||
name: string,
|
||||
title: string,
|
||||
getter: (value: T) => R,
|
||||
compare: Compare<R>,
|
||||
) {
|
||||
return new Column<T>(
|
||||
name,
|
||||
title,
|
||||
(a, b) => compare(getter(a), getter(b)),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
4
src/main/angular/src/app/common/sorter/Direction.ts
Normal file
4
src/main/angular/src/app/common/sorter/Direction.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum Direction {
|
||||
ASC = "ASC",
|
||||
DESC = "DESC",
|
||||
}
|
||||
20
src/main/angular/src/app/common/sorter/Order.ts
Normal file
20
src/main/angular/src/app/common/sorter/Order.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {Column} from "./Column";
|
||||
import {Direction} from './Direction';
|
||||
|
||||
export class Order<T> {
|
||||
|
||||
constructor(
|
||||
public column: Column<T>,
|
||||
public direction: Direction = Direction.ASC,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
toJson(): {} {
|
||||
return {
|
||||
property: this.column.name,
|
||||
direction: this.direction,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
65
src/main/angular/src/app/common/sorter/Sorter.ts
Normal file
65
src/main/angular/src/app/common/sorter/Sorter.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import {Order} from "./Order";
|
||||
import {Column} from "./Column";
|
||||
import {Direction} from './Direction';
|
||||
|
||||
export abstract class Sorter<T> {
|
||||
|
||||
protected constructor(
|
||||
private _orders: Order<T>[] = [],
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
get orders(): Order<T>[] {
|
||||
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<T>): 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<T>): boolean {
|
||||
return this.orders.some(o => o.column === column && o.direction === Direction.ASC);
|
||||
}
|
||||
|
||||
isDescending(column: Column<T>): boolean {
|
||||
return this.orders.some(o => o.column === column && o.direction === Direction.DESC);
|
||||
}
|
||||
|
||||
indexOf(column: Column<T>) {
|
||||
return this.orders.findIndex(o => o.column === column);
|
||||
}
|
||||
|
||||
showCount(column: Column<T>): boolean {
|
||||
return this.count > 1 && this.orders.some(o => o.column === column);
|
||||
}
|
||||
|
||||
}
|
||||
27
src/main/angular/src/app/config/Config.ts
Normal file
27
src/main/angular/src/app/config/Config.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/angular/src/app/config/SeriesListConfig.ts
Normal file
34
src/main/angular/src/app/config/SeriesListConfig.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
25
src/main/angular/src/app/config/TopicListConfig.ts
Normal file
25
src/main/angular/src/app/config/TopicListConfig.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
24
src/main/angular/src/app/config/config.service.ts
Normal file
24
src/main/angular/src/app/config/config.service.ts
Normal file
@ -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<Config>) {
|
||||
this.api.getSingle(["Config", "get"], Config.fromJson, next);
|
||||
}
|
||||
|
||||
set(config: Config, next: Next<Config>) {
|
||||
this.api.postSingle(["Config", "set"], config.toJson(), Config.fromJson, next);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<any, any>[][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,
|
||||
|
||||
27
src/main/angular/src/app/series/AllSeriesPointRequest.ts
Normal file
27
src/main/angular/src/app/series/AllSeriesPointRequest.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
74
src/main/angular/src/app/series/AllSeriesPointResponse.ts
Normal file
74
src/main/angular/src/app/series/AllSeriesPointResponse.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<Series> {
|
||||
|
||||
readonly value: Value;
|
||||
|
||||
constructor(
|
||||
readonly id: number,
|
||||
readonly version: number,
|
||||
@ -12,9 +15,12 @@ export class Series extends ID<Series> {
|
||||
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<Series> {
|
||||
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<Series> {
|
||||
}
|
||||
|
||||
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<Series> {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
equals2() {
|
||||
|
||||
isOld(now: Date, toleranceSeconds: number) {
|
||||
return (now?.getTime() - (this.date?.getTime() || 0)) > (this.expectedEverySeconds + toleranceSeconds) * 1000;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
40
src/main/angular/src/app/series/SeriesPoint.ts
Normal file
40
src/main/angular/src/app/series/SeriesPoint.ts
Normal file
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
45
src/main/angular/src/app/series/SeriesSorter.ts
Normal file
45
src/main/angular/src/app/series/SeriesSorter.ts
Normal file
@ -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<Series> {
|
||||
|
||||
static readonly nam: Column<Series> = Column.create("name", "Name", s => s.name, compareStrings);
|
||||
|
||||
static readonly icon: Column<Series> = Column.create("icon", "Icon", s => s.name, compareStrings);
|
||||
|
||||
static readonly type: Column<Series> = Column.create("delta", "Type", s => s.type, compareStrings);
|
||||
|
||||
static readonly unit: Column<Series> = Column.create("unit", "Einheit", s => s.unit, compareStrings);
|
||||
|
||||
static readonly date: Column<Series> = Column.create("first", "Zuerst", s => s.date, compareNullable(compareDates));
|
||||
|
||||
static readonly decimals: Column<Series> = Column.create("decimals", "Stellen", s => s.decimals, compareNumbers);
|
||||
|
||||
static readonly value: Column<Series> = Column.create("value", "Wert", s => s.value.value, compareNullable(compareNumbers));
|
||||
|
||||
static COLUMNS: Column<Series>[] = [this.nam, this.icon, this.type, this.unit, this.date, this.decimals, this.value];
|
||||
|
||||
private static getColumn(name: string): Column<Series> {
|
||||
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<Series>[] = [],
|
||||
) {
|
||||
super(orders);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
<div class="ChoiceList">
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.interval === null" (click)="setInterval(null)">Live</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.interval === Interval.FIVE" (click)="setInterval(Interval.FIVE)">5 Min.</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.interval === Interval.HOUR" (click)="setInterval(Interval.HOUR)">Stunde</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.interval === Interval.DAY" (click)="setInterval(Interval.DAY)">Tag</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.interval === Interval.WEEK" (click)="setInterval(Interval.WEEK)">Woche</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.interval === Interval.MONTH" (click)="setInterval(Interval.MONTH)">Monat</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.interval === Interval.YEAR" (click)="setInterval(Interval.YEAR)">Jahr</div>
|
||||
</div>
|
||||
|
||||
<div class="ChoiceList" [class.ChoiceListInactive]="config.seriesList.interval === null">
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 7" (click)="setOffset(7)">-7</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 6" (click)="setOffset(6)">-6</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 5" (click)="setOffset(5)">-5</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 4" (click)="setOffset(4)">-4</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 3" (click)="setOffset(3)">-3</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 2" (click)="setOffset(2)">-2</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 1" (click)="setOffset(1)">-1</div>
|
||||
<div class="Choice" [class.ChoiceActive]="config.seriesList.offset === 0" (click)="setOffset(0)">0</div>
|
||||
</div>
|
||||
|
||||
<input type="text" [(ngModel)]="config.seriesList.search" (ngModelChange)="saveConfig(false)" placeholder="Filter">
|
||||
|
||||
<table>
|
||||
|
||||
<tr>
|
||||
<th app-column class="name" [column]="SeriesSorter.nam" [sorter]="config.seriesList.sorter" (onChange)="saveConfig(false)"></th>
|
||||
<th app-column class="value" [column]="SeriesSorter.value" [sorter]="config.seriesList.sorter" (onChange)="saveConfig(false)" colspan="3"></th>
|
||||
<th app-column class="unit" [column]="SeriesSorter.unit" [sorter]="config.seriesList.sorter" (onChange)="saveConfig(false)"></th>
|
||||
<th app-column class="type" [column]="SeriesSorter.type" [sorter]="config.seriesList.sorter" (onChange)="saveConfig(false)"></th>
|
||||
</tr>
|
||||
|
||||
@if (config.seriesList.interval === null) {
|
||||
@for (series of seriesFiltered; track series.id) {
|
||||
<tr class="Series" [class.SeriesOld]="series.isOld(now,5)">
|
||||
<td class="name">{{ series.name }}</td>
|
||||
<td class="valueInteger">{{ series.value.integer }}</td>
|
||||
@if (series.value.showDot) {
|
||||
<td class="valueDot">,</td>
|
||||
} @else {
|
||||
<td class="valueDot"></td>
|
||||
}
|
||||
<td class="valueFraction">{{ series.value.fraction }}</td>
|
||||
<td class="unit">{{ series.unit }}</td>
|
||||
@switch (series.type) {
|
||||
@case (SeriesType.VARYING) {
|
||||
<td class="type">⌀</td>
|
||||
}
|
||||
@case (SeriesType.DELTA) {
|
||||
<td class="type">Δ</td>
|
||||
}
|
||||
@case (SeriesType.BOOL) {
|
||||
<td class="type">B</td>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
@if (config.seriesList.interval !== null) {
|
||||
@for (seriesPoint of aggregateFiltered; track seriesPoint.series.id) {
|
||||
<tr class="Series">
|
||||
<td class="name">{{ seriesPoint.series.name }}</td>
|
||||
<td class="valueInteger">{{ seriesPoint.avg.integer }}</td>
|
||||
@if (seriesPoint.avg.showDot) {
|
||||
<td class="valueDot">,</td>
|
||||
} @else {
|
||||
<td class="valueDot"></td>
|
||||
}
|
||||
<td class="valueFraction">{{ seriesPoint.avg.fraction }}</td>
|
||||
<td class="unit">{{ seriesPoint.series.unit }}</td>
|
||||
<td class="type">Δ</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
</table>
|
||||
|
||||
@if (config.seriesList.interval === null) {
|
||||
<div class="hint Counts">{{ seriesFiltered.length }} / {{ seriesUnfiltered.length }}</div>
|
||||
} @else {
|
||||
<div class="hint Counts">{{ aggregateFiltered.length }} / {{ aggregateUnfiltered.length }}</div>
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
155
src/main/angular/src/app/series/list/series-list.component.ts
Normal file
155
src/main/angular/src/app/series/list/series-list.component.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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<Series> {
|
||||
super(api, ['Series'], Series.fromJson, Series.equals, Series.compareName);
|
||||
}
|
||||
|
||||
points(series: Series, interval: Interval, offset: number, duration: number, next: Next<number[][]>): void {
|
||||
oneSeriesPoints(series: Series, interval: Interval, offset: number, duration: number, next: Next<number[][]>): 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<Series>) {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
26
src/main/angular/src/app/series/varying/Varying.ts
Normal file
26
src/main/angular/src/app/series/varying/Varying.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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'
|
||||
|
||||
4
src/main/angular/src/app/topic/TimestampType.ts
Normal file
4
src/main/angular/src/app/topic/TimestampType.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum TimestampType {
|
||||
EPOCH_MILLISECONDS = "EPOCH_MILLISECONDS",
|
||||
EPOCH_SECONDS = "EPOCH_SECONDS",
|
||||
}
|
||||
44
src/main/angular/src/app/topic/Topic.ts
Normal file
44
src/main/angular/src/app/topic/Topic.ts
Normal file
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
28
src/main/angular/src/app/topic/TopicQuery.ts
Normal file
28
src/main/angular/src/app/topic/TopicQuery.ts
Normal file
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
5
src/main/angular/src/app/topic/TopicQueryFunction.ts
Normal file
5
src/main/angular/src/app/topic/TopicQueryFunction.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum TopicQueryFunction {
|
||||
NONE = "NONE",
|
||||
ONLY_POSITIVE = "ONLY_POSITIVE",
|
||||
ONLY_NEGATIVE_BUT_NEGATE = "ONLY_NEGATIVE_BUT_NEGATE",
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<table class="TopicList">
|
||||
@for (topic of sorted(); track topic.id) {
|
||||
<tr class="head">
|
||||
<td class="name">{{ topic.name }}</td>
|
||||
<td class="last">{{ topic.last | date:'long':'':'de-DE' }}</td>
|
||||
<td class="payload" [class.empty]="topic.payload === null" colspan="6">{{ topic.payload }}</td>
|
||||
</tr>
|
||||
<tr class="timestamp">
|
||||
<td class="timestampQuery" [class.empty]="!topic.timestampQuery">{{ topic.timestampQuery }}</td>
|
||||
<td class="timestampType" [class.empty]="topic.timestampType === null">{{ topic.timestampType }}</td>
|
||||
<td class="timestampLast" [class.empty]="topic.timestampLast === null" colspan="6">{{ topic.timestampLast | date:'long':'':'de-DE' }}</td>
|
||||
</tr>
|
||||
@for (query of topic.queries; track $index) {
|
||||
<tr class="query">
|
||||
<td class="valueQuery" [class.empty]="query.valueQuery === null">{{ query.valueQuery }}</td>
|
||||
<td class="valueFunction" [class.empty]="query.func === null">{{ query.func }}</td>
|
||||
<td class="valueSeries" [class.empty]="query.series.name === null">{{ query.series.name }}</td>
|
||||
<td class="valueSeriesValueInteger" [class.empty]="query.series.value.value === null">{{ query.series.value.integer }}</td>
|
||||
@if (query.series.value.showDot) {
|
||||
<td class="valueSeriesValueDot" [class.empty]="query.series.value.value === null">,</td>
|
||||
} @else {
|
||||
<td class="valueSeriesValueDot" [class.empty]="query.series.value.value === null"></td>
|
||||
}
|
||||
<td class="valueSeriesValueFraction" [class.empty]="query.series.value.value === null">{{ query.series.value.fraction }}</td>
|
||||
<td class="valueSeriesValueUnit" [class.empty]="query.series.value.value === null">{{ query.series.unit }}</td>
|
||||
<td class="valueSeriesDate" [class.empty]="query.series.date === null">{{ query.series.date | date:'long':'':'de-DE' }}</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="spacer">
|
||||
<td colspan="8"> </td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
104
src/main/angular/src/app/topic/list/topic-list.component.less
Normal file
104
src/main/angular/src/app/topic/list/topic-list.component.less
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
91
src/main/angular/src/app/topic/list/topic-list.component.ts
Normal file
91
src/main/angular/src/app/topic/list/topic-list.component.ts
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
16
src/main/angular/src/app/topic/topic.service.ts
Normal file
16
src/main/angular/src/app/topic/topic.service.ts
Normal file
@ -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<Topic> {
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
) {
|
||||
super(api, ["Topic"], Topic.fromJson);
|
||||
}
|
||||
|
||||
}
|
||||
2
src/main/angular/src/config.less
Normal file
2
src/main/angular/src/config.less
Normal file
@ -0,0 +1,2 @@
|
||||
@space: 0.25em;
|
||||
@border: 0.01em;
|
||||
85
src/main/java/de/ph87/data/config/Config.java
Normal file
85
src/main/java/de/ph87/data/config/Config.java
Normal file
@ -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<Order> 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();
|
||||
}
|
||||
|
||||
}
|
||||
30
src/main/java/de/ph87/data/config/ConfigController.java
Normal file
30
src/main/java/de/ph87/data/config/ConfigController.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
28
src/main/java/de/ph87/data/config/ConfigDto.java
Normal file
28
src/main/java/de/ph87/data/config/ConfigDto.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
11
src/main/java/de/ph87/data/config/ConfigRepository.java
Normal file
11
src/main/java/de/ph87/data/config/ConfigRepository.java
Normal file
@ -0,0 +1,11 @@
|
||||
package de.ph87.data.config;
|
||||
|
||||
import org.springframework.data.repository.ListCrudRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ConfigRepository extends ListCrudRepository<Config, Long> {
|
||||
|
||||
Optional<Config> findFirstBy();
|
||||
|
||||
}
|
||||
52
src/main/java/de/ph87/data/config/ConfigSeriesListDto.java
Normal file
52
src/main/java/de/ph87/data/config/ConfigSeriesListDto.java
Normal file
@ -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<OrderDto> 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<OrderDto> 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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/java/de/ph87/data/config/ConfigService.java
Normal file
26
src/main/java/de/ph87/data/config/ConfigService.java
Normal file
@ -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))));
|
||||
}
|
||||
|
||||
}
|
||||
47
src/main/java/de/ph87/data/config/ConfigTopicListDto.java
Normal file
47
src/main/java/de/ph87/data/config/ConfigTopicListDto.java
Normal file
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
5
src/main/java/de/ph87/data/config/Direction.java
Normal file
5
src/main/java/de/ph87/data/config/Direction.java
Normal file
@ -0,0 +1,5 @@
|
||||
package de.ph87.data.config;
|
||||
|
||||
public enum Direction {
|
||||
ASC, DESC
|
||||
}
|
||||
37
src/main/java/de/ph87/data/config/Order.java
Normal file
37
src/main/java/de/ph87/data/config/Order.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
29
src/main/java/de/ph87/data/config/OrderDto.java
Normal file
29
src/main/java/de/ph87/data/config/OrderDto.java
Normal file
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
34
src/main/java/de/ph87/data/series/AllSeriesPointRequest.java
Normal file
34
src/main/java/de/ph87/data/series/AllSeriesPointRequest.java
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<Entry> seriesPoints;
|
||||
|
||||
@Data
|
||||
public static class Entry {
|
||||
|
||||
@NonNull
|
||||
public final SeriesDto series;
|
||||
|
||||
@Nullable
|
||||
public final SeriesPoint point;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
19
src/main/java/de/ph87/data/series/ISeriesPointRequest.java
Normal file
19
src/main/java/de/ph87/data/series/ISeriesPointRequest.java
Normal file
@ -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();
|
||||
|
||||
}
|
||||
@ -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,
|
||||
@ -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<? extends SeriesPoint> points;
|
||||
|
||||
}
|
||||
@ -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<OneSeriesPointsResponse> {
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -32,9 +32,15 @@ public class SeriesController {
|
||||
return seriesRepository.getDtoById(id);
|
||||
}
|
||||
|
||||
@PostMapping("points")
|
||||
public List<? extends SeriesPoint> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<SeriesPoint> {
|
||||
|
||||
@Override
|
||||
public void serialize(final SeriesPoint seriesPoint, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException {
|
||||
jsonGenerator.writeStartArray();
|
||||
seriesPoint.toJson(jsonGenerator);
|
||||
jsonGenerator.writeEndArray();
|
||||
}
|
||||
|
||||
}
|
||||
@ -26,8 +26,27 @@ public class SeriesService {
|
||||
private final VaryingService varyingService;
|
||||
|
||||
@NonNull
|
||||
public List<? extends SeriesPoint> 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<AllSeriesPointResponse.Entry> 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<? extends SeriesPoint> 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<? extends SeriesPoint> 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);
|
||||
|
||||
@ -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<BoolPoint> points(@NonNull final Series series, @NonNull final SeriesPointsRequest request) {
|
||||
return boolRepo.points(series, request.first, request.after);
|
||||
public List<BoolPoint> points(@NonNull final Series series, @NonNull final ISeriesPointRequest request) {
|
||||
return boolRepo.points(series, request.getFirst(), request.getAfter());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<DeltaPoint> 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<DeltaPoint> 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());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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<VaryingPoint> 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<VaryingPoint> 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());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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<TopicQuery> 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++;
|
||||
}
|
||||
|
||||
|
||||
@ -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<TopicQueryDto> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -37,9 +37,12 @@ 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);
|
||||
final Topic topic = updateOrCreate(inbound.topic, inbound.payload);
|
||||
try {
|
||||
if (!topic.isEnabled()) {
|
||||
log.debug("Topic is not enabled: topic={}", topic);
|
||||
return;
|
||||
@ -71,7 +74,11 @@ public class TopicReceiver {
|
||||
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
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user