Browse Source

UI: Add offse hint in CF; Improvement time unit component

pull/14253/head
Vladyslav_Prykhodko 10 months ago
parent
commit
6645b082b2
  1. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  2. 209
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  3. 42
      ui-ngx/src/app/shared/components/time-unit-input.component.ts
  4. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

7
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html

@ -71,7 +71,7 @@
labelText="{{ 'calculated-fields.aggregate-interval-value' | translate }}" labelText="{{ 'calculated-fields.aggregate-interval-value' | translate }}"
minErrorText="{{ 'calculated-fields.aggregate-interval-value-min' | translate : {sec: minAggregationIntervalInSecForCF} }}" minErrorText="{{ 'calculated-fields.aggregate-interval-value-min' | translate : {sec: minAggregationIntervalInSecForCF} }}"
requiredText="{{ 'calculated-fields.aggregate-interval-value-required' | translate }}" requiredText="{{ 'calculated-fields.aggregate-interval-value-required' | translate }}"
stepMultipleOfErrorText="Must be 1 day" stepMultipleOfErrorText="{{ 'calculated-fields.aggregate-interval-value-step-multiple-of' | translate }}"
formControlName="durationSec"> formControlName="durationSec">
</tb-time-unit-input> </tb-time-unit-input>
} }
@ -84,15 +84,20 @@
@if (entityAggregationConfiguration.get('interval.allowOffsetSec').value) { @if (entityAggregationConfiguration.get('interval.allowOffsetSec').value) {
<tb-time-unit-input required <tb-time-unit-input required
[minTime]="0" [minTime]="0"
[maxTime]="maxOffsetTime"
sameWidthInputs sameWidthInputs
appearance="outline" appearance="outline"
subscriptSizing="dynamic" subscriptSizing="dynamic"
containerClass="flex gap-3" containerClass="flex gap-3"
labelText="{{ 'calculated-fields.entity-aggregation.offset-value' | translate }}" labelText="{{ 'calculated-fields.entity-aggregation.offset-value' | translate }}"
minErrorText="{{ 'calculated-fields.entity-aggregation.offset-value-min' | translate }}" minErrorText="{{ 'calculated-fields.entity-aggregation.offset-value-min' | translate }}"
maxErrorText="{{ 'calculated-fields.entity-aggregation.offset-value-max' | translate }}"
requiredText="{{ 'calculated-fields.entity-aggregation.offset-value-required' | translate }}" requiredText="{{ 'calculated-fields.entity-aggregation.offset-value-required' | translate }}"
formControlName="offsetSec"> formControlName="offsetSec">
</tb-time-unit-input> </tb-time-unit-input>
<div class="tb-form-hint tb-primary-fill hint-container">
{{ hint }}
</div>
} }
</div> </div>
</ng-container> </ng-container>

209
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts

@ -35,19 +35,29 @@ import {
notEmptyObjectValidator, notEmptyObjectValidator,
OutputType OutputType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { map } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models'; import { AVG_MONTH, AVG_QUARTER, DAY, HOUR, MINUTE, SECOND, YEAR } from '@shared/models/time/time.models';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import _moment from 'moment';
interface CalculatedFieldEntityAggregationConfigurationValue extends CalculatedFieldEntityAggregationConfiguration { interface CalculatedFieldEntityAggregationConfigurationValue extends CalculatedFieldEntityAggregationConfiguration {
interval: AggInterval & {allowOffsetSec?: boolean}; interval: AggInterval & {allowOffsetSec?: boolean};
allowWatermark: boolean; allowWatermark: boolean;
} }
enum TimeCategory {
SECONDS = 'SECONDS',
MINUTES = 'MINUTES',
HOURS = 'HOURS',
DAYS = 'DAYS'
}
@Component({ @Component({
selector: 'tb-entity-aggregation-component', selector: 'tb-entity-aggregation-component',
templateUrl: './entity-aggregation-component.component.html', templateUrl: './entity-aggregation-component.component.html',
@ -86,7 +96,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
tz: ['', Validators.required], tz: ['', Validators.required],
durationSec: [this.minAggregationIntervalInSecForCF, Validators.required], durationSec: [this.minAggregationIntervalInSecForCF, Validators.required],
allowOffsetSec: [false], allowOffsetSec: [false],
offsetSec: [MINUTE/SECOND, Validators.required], offsetSec: [this.minAggregationIntervalInSecForCF > 60 ? MINUTE / SECOND : 1, Validators.required],
}), }),
allowWatermark: [false], allowWatermark: [false],
watermark: this.fb.group({ watermark: this.fb.group({
@ -105,10 +115,13 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
AggIntervalTypes = Object.values(AggIntervalType) as AggIntervalType[]; AggIntervalTypes = Object.values(AggIntervalType) as AggIntervalType[];
AggIntervalTypeTranslations = AggIntervalTypeTranslations; AggIntervalTypeTranslations = AggIntervalTypeTranslations;
hint: string;
private propagateChange: (config: CalculatedFieldEntityAggregationConfiguration) => void = () => { }; private propagateChange: (config: CalculatedFieldEntityAggregationConfiguration) => void = () => { };
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private store: Store<AppState>) { private store: Store<AppState>,
private translate: TranslateService,) {
this.entityAggregationConfiguration.get('interval.type').valueChanges.pipe( this.entityAggregationConfiguration.get('interval.type').valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
@ -128,6 +141,18 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
this.checkWatermark(allow); this.checkWatermark(allow);
}); });
merge(
this.entityAggregationConfiguration.get('interval.type').valueChanges,
this.entityAggregationConfiguration.get('interval.durationSec').valueChanges,
this.entityAggregationConfiguration.get('interval.offsetSec').valueChanges,
this.entityAggregationConfiguration.get('interval.allowOffsetSec').valueChanges,
).pipe(
filter(() => this.entityAggregationConfiguration.get('interval.allowOffsetSec').value),
takeUntilDestroyed()
).subscribe(() => {
this.updatedOffsetHint();
});
this.entityAggregationConfiguration.valueChanges.pipe( this.entityAggregationConfiguration.valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { ).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => {
@ -149,6 +174,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value);
this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value);
this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value);
this.updatedOffsetHint();
setTimeout(() => { setTimeout(() => {
this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
}); });
@ -171,6 +197,26 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
} }
} }
get maxOffsetTime(): number {
switch (this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType) {
case AggIntervalType.HOUR:
return HOUR / SECOND - 1;
case AggIntervalType.DAY:
return DAY / SECOND - 1;
case AggIntervalType.WEEK:
case AggIntervalType.WEEK_SUN_SAT:
return 7 * DAY / SECOND - 1;
case AggIntervalType.MONTH:
return AVG_MONTH / SECOND;
case AggIntervalType.QUARTER:
return AVG_QUARTER / SECOND - 1;
case AggIntervalType.YEAR:
return YEAR / SECOND - 1;
case AggIntervalType.CUSTOM:
return this.entityAggregationConfiguration.get('interval.durationSec').value - 1;
}
}
private updatedModel(value: CalculatedFieldEntityAggregationConfigurationValue): void { private updatedModel(value: CalculatedFieldEntityAggregationConfigurationValue): void {
value.type = CalculatedFieldType.ENTITY_AGGREGATION; value.type = CalculatedFieldType.ENTITY_AGGREGATION;
if (!value.interval.allowOffsetSec) { if (!value.interval.allowOffsetSec) {
@ -197,6 +243,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
this.entityAggregationConfiguration.get('interval.offsetSec').enable({emitEvent: false}); this.entityAggregationConfiguration.get('interval.offsetSec').enable({emitEvent: false});
} else { } else {
this.entityAggregationConfiguration.get('interval.offsetSec').disable({emitEvent: false}); this.entityAggregationConfiguration.get('interval.offsetSec').disable({emitEvent: false});
this.hint = '';
} }
} }
@ -207,4 +254,158 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
this.entityAggregationConfiguration.get('watermark').disable({emitEvent: false}); this.entityAggregationConfiguration.get('watermark').disable({emitEvent: false});
} }
} }
private updatedOffsetHint(): void {
const offset = this.entityAggregationConfiguration.get('interval.offsetSec').value;
const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType;
const durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value;
const offsetCategory = this.getTimeCategory(offset);
const now = _moment.utc();
let interval: string = '';
if (intervalType === AggIntervalType.CUSTOM) {
const durationSecCategory = this.getTimeCategory(durationSec);
const formatString = this.getCustomFormatString(offsetCategory, durationSecCategory);
const intervals: string[] = [];
let allInterval = durationSec >= HOUR*6/SECOND && durationSec < DAY/SECOND;
now.startOf('year').add(offset, 'seconds');
let repeat = 2;
if (allInterval) {
repeat = Math.floor(DAY/SECOND/durationSec);
if (repeat > 4) {
repeat = 2;
allInterval = false;
}
}
for (let i = 0; i < repeat; i++) {
const s1 = now.clone().add(i * durationSec, 'seconds').format(formatString);
const s2 = now.clone().add((i + 1) * durationSec, 'seconds').format(formatString);
intervals.push(`${s1} - ${s2}`);
}
interval = intervals.join('; ');
if (allInterval) {
this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset', {interval});
} else {
interval += '…'
this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset-and-so-on', {interval});
}
} else {
interval = this.buildStandardIntervalString(now, intervalType, offset, offsetCategory);
this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset-and-so-on', { interval });
}
}
private getTimeCategory(seconds: number): TimeCategory {
if (seconds % (DAY / SECOND) === 0) {
return TimeCategory.DAYS;
}
if (seconds % (HOUR / SECOND) === 0) {
return TimeCategory.HOURS;
}
if (seconds % (MINUTE / SECOND) === 0) {
return TimeCategory.MINUTES;
}
return TimeCategory.SECONDS;
}
private getCustomFormatString(offsetCat: TimeCategory, durationCat: TimeCategory): string {
if (durationCat === TimeCategory.DAYS) {
if (offsetCat === TimeCategory.SECONDS) {
return '[Day] D, HH:mm:ss';
}
if (offsetCat === TimeCategory.MINUTES || offsetCat === TimeCategory.HOURS) {
return '[Day] D, HH:mm';
}
return '[Day] D';
} else {
if (offsetCat === TimeCategory.SECONDS) {
return 'HH:mm:ss';
}
return 'HH:mm';
}
}
private formatAdditiveInterval(now: _moment.Moment, addUnit: 'hour' | 'day' | 'month' | 'quarter', offsetCat: TimeCategory,
formats: { [key in TimeCategory]?: { s1: string, s2: string, s3: string } }): string {
const formatTs = formats[offsetCat] || formats[TimeCategory.SECONDS];
if (!formatTs) {
return '';
}
const s1 = now.format(formatTs.s1);
const s2 = now.clone().add(1, addUnit).format(formatTs.s2);
const s3 = now.clone().add(2, addUnit).format(formatTs.s3);
return `${s1} - ${s2}; ${s2} - ${s3}`;
}
private formatNextInterval(now: _moment.Moment, offsetCat: TimeCategory, secFmt: string, minHourFmt: string, dayFmt: string): string {
let s1: string;
if (offsetCat === TimeCategory.SECONDS) {
s1 = now.format(secFmt);
} else if (offsetCat === TimeCategory.MINUTES || offsetCat === TimeCategory.HOURS) {
s1 = now.format(minHourFmt);
} else {
s1 = now.format(dayFmt);
}
const s2 = `Next ${s1}`;
const s3 = `Following ${s1}`;
return `${s1} - ${s2}; ${s2} - ${s3}`;
}
private buildStandardIntervalString(now: _moment.Moment, type: AggIntervalType, offset: number, offsetCat: TimeCategory): string {
switch (type) {
case AggIntervalType.HOUR:
now.startOf('day').add(offset, 'seconds');
return this.formatAdditiveInterval(now, 'hour', offsetCat, {
[TimeCategory.SECONDS]: { s1: 'HH:mm:ss', s2: 'HH:mm:ss', s3: 'HH:mm:ss' },
[TimeCategory.MINUTES]: { s1: 'HH:mm:ss', s2: 'HH:mm', s3: 'HH:mm' }
});
case AggIntervalType.DAY:
now.startOf('month').add(offset, 'seconds');
return this.formatAdditiveInterval(now, 'day', offsetCat, {
[TimeCategory.SECONDS]: { s1: '[Day] D, HH:mm:ss', s2: '[Day] D, HH:mm:ss', s3: '[Day] D, HH:mm:ss' },
[TimeCategory.MINUTES]: { s1: '[Day] D, HH:mm:ss', s2: '[Day] D, HH:mm', s3: '[Day] D, HH:mm' },
[TimeCategory.HOURS]: { s1: 'HH:mm:ss', s2: '[Day] D, HH:mm', s3: '[Day] D, HH:mm' } // Note: Original logic, s1 format is different
});
case AggIntervalType.WEEK:
now.isoWeekday(1).startOf('isoWeek').add(offset, 'seconds');
return this.formatNextInterval(now, offsetCat, 'ddd, HH:mm:ss', 'ddd, HH:mm', 'ddd');
case AggIntervalType.WEEK_SUN_SAT:
now.startOf('week').add(offset, 'seconds');
return this.formatNextInterval(now, offsetCat, 'ddd, HH:mm:ss', 'ddd, HH:mm', 'ddd');
case AggIntervalType.MONTH:
now.startOf('year').add(offset, 'seconds');
return this.formatAdditiveInterval(now, 'month', offsetCat, {
[TimeCategory.SECONDS]: { s1: 'Do [of month], HH:mm:ss', s2: '[Next] Do, HH:mm:ss', s3: '[Following] Do, HH:mm:ss' },
[TimeCategory.MINUTES]: { s1: 'Do [of month], HH:mm', s2: '[Next] Do, HH:mm', s3: '[Following] Do, HH:mm' },
[TimeCategory.HOURS]: { s1: 'Do [of month], HH:mm', s2: '[Next] Do, HH:mm', s3: '[Following] Do, HH:mm' },
[TimeCategory.DAYS]: { s1: 'Do [of month]', s2: '[Next] Do', s3: '[Following] Do' }
});
case AggIntervalType.QUARTER:
now.startOf('year').add(offset, 'seconds');
return this.formatAdditiveInterval(now, 'quarter', offsetCat, {
[TimeCategory.SECONDS]: { s1: 'MMM Do, HH:mm:ss', s2: 'MMM Do, HH:mm:ss', s3: 'MMM Do, HH:mm:ss' },
[TimeCategory.MINUTES]: { s1: 'MMM Do, HH:mm', s2: 'MMM Do, HH:mm', s3: 'MMM Do, HH:mm' },
[TimeCategory.HOURS]: { s1: 'MMM Do, HH:mm', s2: 'MMM Do, HH:mm', s3: 'MMM Do, HH:mm' },
[TimeCategory.DAYS]: { s1: 'MMM Do', s2: 'MMM Do', s3: 'MMM Do' }
});
case AggIntervalType.YEAR:
now.startOf('year').add(offset, 'seconds');
return this.formatNextInterval(now, offsetCat, 'MMM Do, HH:mm:ss', 'MMM Do, HH:mm', 'MMM Do');
default:
return '';
}
}
} }

42
ui-ngx/src/app/shared/components/time-unit-input.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
@ -51,7 +51,7 @@ interface TimeUnitInputModel {
multi: true multi: true
}] }]
}) })
export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit { export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input() @Input()
labelText: string; labelText: string;
@ -129,15 +129,8 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
} }
ngOnInit() { ngOnInit() {
if (this.maxTime) { if (isDefinedAndNotNull(this.maxTime)) {
const maxTimeMs = this.maxTime * SECOND; this.updatedAllowTimeUnitInterval(this.maxTime);
if (maxTimeMs < MINUTE) {
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS);
} else if (maxTimeMs < HOUR) {
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS);
} else if (maxTimeMs < DAY) {
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS);
}
} }
if (this.required || this.maxTime || isDefinedAndNotNull(this.minTime) || this.stepMultipleOf) { if (this.required || this.maxTime || isDefinedAndNotNull(this.minTime) || this.stepMultipleOf) {
const timeControl = this.timeInputForm.get('time'); const timeControl = this.timeInputForm.get('time');
@ -190,6 +183,21 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
} }
} }
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'maxTime') {
if (isDefinedAndNotNull(this.maxTime)) {
this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
this.updatedAllowTimeUnitInterval(this.maxTime);
this.timeInputForm.get('time').updateValueAndValidity({emitEvent: false});
}
}
}
}
}
registerOnChange(fn: any) { registerOnChange(fn: any) {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -279,4 +287,16 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
}; };
} }
private updatedAllowTimeUnitInterval(maxTime: number) {
const maxTimeMs = maxTime * SECOND;
this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
if (maxTimeMs < MINUTE) {
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS);
} else if (maxTimeMs < HOUR) {
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS);
} else if (maxTimeMs < DAY) {
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS);
}
}
} }

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1210,6 +1210,8 @@
"year": "Year", "year": "Year",
"custom": "Custom" "custom": "Custom"
}, },
"aggregate-period-hint-offset": "Your aggregation interval will be: {{ interval }}",
"aggregate-period-hint-offset-and-so-on": "Your aggregation interval will be: {{ interval }} and so on",
"entity-aggregation": { "entity-aggregation": {
"argument-hint": "Data will be fetched from selected entity", "argument-hint": "Data will be fetched from selected entity",
"argument-setting-hint": "Latest telemetry is the only available argument type for this calculated field", "argument-setting-hint": "Latest telemetry is the only available argument type for this calculated field",
@ -1220,6 +1222,7 @@
"offset-value": "Offset value", "offset-value": "Offset value",
"offset-value-required": "Offset value is required", "offset-value-required": "Offset value is required",
"offset-value-min": "Offset value must be a positive integer", "offset-value-min": "Offset value must be a positive integer",
"offset-value-max": "Offset value should be less than the aggregate interval value",
"wait-delay": "Wait for delayed telemetry", "wait-delay": "Wait for delayed telemetry",
"wait-delay-hint": "Waits for delayed telemetry after the interval ends", "wait-delay-hint": "Waits for delayed telemetry after the interval ends",
"duration": "Duration", "duration": "Duration",

Loading…
Cancel
Save