Browse Source

Merge pull request #2 from vvlladd28/feature/rulenode/save-ts/ts-strategy

UI: Persistence settings to save ts rule node
pull/12413/head
Dmytro Skarzhynets 2 years ago
committed by GitHub
parent
commit
a463707d5b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts
  2. 39
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.html
  3. 114
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.ts
  4. 31
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html
  5. 83
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts
  6. 96
      ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html
  7. 105
      ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts
  8. 78
      ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.models.ts
  9. 7
      ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts
  10. 44
      ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html
  11. 184
      ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts
  12. 28
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts

@ -42,6 +42,12 @@ import { DeleteAttributesConfigComponent } from './delete-attributes-config.comp
import { MathFunctionConfigComponent } from './math-function-config.component'; import { MathFunctionConfigComponent } from './math-function-config.component';
import { DeviceStateConfigComponent } from './device-state-config.component'; import { DeviceStateConfigComponent } from './device-state-config.component';
import { SendRestApiCallReplyConfigComponent } from './send-rest-api-call-reply-config.component'; import { SendRestApiCallReplyConfigComponent } from './send-rest-api-call-reply-config.component';
import {
AdvancedPersistenceSettingComponent
} from '@home/components/rule-node/action/advanced-persistence-setting.component';
import {
AdvancedPersistenceSettingRowComponent
} from '@home/components/rule-node/action/advanced-persistence-setting-row.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -67,7 +73,9 @@ import { SendRestApiCallReplyConfigComponent } from './send-rest-api-call-reply-
PushToEdgeConfigComponent, PushToEdgeConfigComponent,
PushToCloudConfigComponent, PushToCloudConfigComponent,
MathFunctionConfigComponent, MathFunctionConfigComponent,
DeviceStateConfigComponent DeviceStateConfigComponent,
AdvancedPersistenceSettingComponent,
AdvancedPersistenceSettingRowComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,

39
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.html

@ -0,0 +1,39 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="persistenceSettingRowForm" class="tb-form-panel stroked no-gap no-padding-bottom">
<div class="tb-form-panel-title mb-4">{{ title }}</div>
<mat-form-field>
<mat-label translate>rule-node-config.save-time-series.strategy</mat-label>
<mat-select formControlName="type">
@for (strategy of persistenceStrategies; track strategy) {
<mat-option [value]="strategy">{{ PersistenceTypeTranslationMap.get(strategy) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
@if(persistenceSettingRowForm.get('type').value === PersistenceType.DEDUPLICATE) {
<tb-time-unit-input
required
labelText="{{ 'rule-node-config.save-time-series.deduplication-interval' | translate }}"
requiredText="{{ 'rule-node-config.save-time-series.deduplication-interval-required' | translate }}"
minErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
maxErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
[maxTime]="maxDeduplicateTime"
formControlName="deduplicationIntervalSecs">
</tb-time-unit-input>
}
</section>

114
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.ts

@ -0,0 +1,114 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import {
AdvancedPersistenceConfig,
defaultAdvancedPersistenceConfig,
maxDeduplicateTimeSecs,
PersistenceType,
PersistenceTypeTranslationMap
} from '@home/components/rule-node/action/timeseries-config.models';
import { isDefinedAndNotNull } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-advanced-persistence-setting-row',
templateUrl: './advanced-persistence-setting-row.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AdvancedPersistenceSettingRowComponent),
multi: true
},{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AdvancedPersistenceSettingRowComponent),
multi: true
}]
})
export class AdvancedPersistenceSettingRowComponent implements ControlValueAccessor, Validator {
@Input()
title: string;
persistenceSettingRowForm = this.fb.group({
type: [defaultAdvancedPersistenceConfig.type],
deduplicationIntervalSecs: [{value: 60, disabled: true}]
});
PersistenceType = PersistenceType;
persistenceStrategies = [PersistenceType.ON_EVERY_MESSAGE, PersistenceType.DEDUPLICATE, PersistenceType.SKIP];
PersistenceTypeTranslationMap = PersistenceTypeTranslationMap;
maxDeduplicateTime = maxDeduplicateTimeSecs;
private propagateChange: (value: any) => void = () => {};
constructor(private fb: FormBuilder) {
this.persistenceSettingRowForm.get('type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(() => this.updatedValidation());
this.persistenceSettingRowForm.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value) => this.propagateChange(value));
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(_fn: any) {
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.persistenceSettingRowForm.disable({emitEvent: false});
} else {
this.persistenceSettingRowForm.enable({emitEvent: false});
this.updatedValidation();
}
}
validate(): ValidationErrors | null {
return this.persistenceSettingRowForm.valid ? null : {
persistenceSettingRow: false
};
}
writeValue(value: AdvancedPersistenceConfig) {
if (isDefinedAndNotNull(value)) {
this.persistenceSettingRowForm.patchValue(value, {emitEvent: false});
} else {
this.persistenceSettingRowForm.patchValue(defaultAdvancedPersistenceConfig);
}
}
private updatedValidation() {
if (this.persistenceSettingRowForm.get('type').value === PersistenceType.DEDUPLICATE) {
this.persistenceSettingRowForm.get('deduplicationIntervalSecs').enable({emitEvent: false});
} else {
this.persistenceSettingRowForm.get('deduplicationIntervalSecs').disable({emitEvent: false})
}
}
}

31
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html

@ -0,0 +1,31 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="persistenceForm" class="tb-form-panel no-border no-padding">
<tb-advanced-persistence-setting-row
formControlName="timeseries"
title="{{ 'rule-node-config.save-time-series.time-series' | translate }}"
></tb-advanced-persistence-setting-row>
<tb-advanced-persistence-setting-row
formControlName="latest"
title="{{ 'rule-node-config.save-time-series.latest' | translate }}"
></tb-advanced-persistence-setting-row>
<tb-advanced-persistence-setting-row
formControlName="webSockets"
title="{{ 'rule-node-config.save-time-series.web-sockets' | translate }}"
></tb-advanced-persistence-setting-row>
</section>

83
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts

@ -0,0 +1,83 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import { Component, forwardRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AdvancedPersistenceStrategy } from '@home/components/rule-node/action/timeseries-config.models';
@Component({
selector: 'tb-advanced-persistence-settings',
templateUrl: './advanced-persistence-setting.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AdvancedPersistenceSettingComponent),
multi: true
},{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AdvancedPersistenceSettingComponent),
multi: true
}]
})
export class AdvancedPersistenceSettingComponent implements ControlValueAccessor, Validator {
persistenceForm = this.fb.group({
timeseries: [null],
latest: [null],
webSockets: [null]
});
private propagateChange: (value: any) => void = () => {};
constructor(private fb: FormBuilder) {
this.persistenceForm.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => this.propagateChange(value));
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(_fn: any) {
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.persistenceForm.disable({emitEvent: false});
} else {
this.persistenceForm.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.persistenceForm.valid ? null : {
persistenceForm: false
};
}
writeValue(value: AdvancedPersistenceStrategy) {
this.persistenceForm.patchValue(value, {emitEvent: false});
}
}

96
ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html

@ -16,35 +16,73 @@
--> -->
<section [formGroup]="timeseriesConfigForm" class="tb-form-panel no-border no-padding"> <section [formGroup]="timeseriesConfigForm" class="tb-form-panel no-border no-padding">
<mat-form-field class="mat-block flex-1"> <div class="tb-form-panel stroked no-padding-bottom no-gap" formGroupName="persistenceSettings">
<mat-label translate>rule-node-config.default-ttl</mat-label> <div class="mb-4 flex flex-row items-center justify-between">
<input type="number" min="0" step="1" matInput formControlName="defaultTTL" required> <div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'rule-node-config.save-time-series.persistence-settings-hint' | translate}}" translate>
<mat-icon class="help-icon margin-8 cursor-pointer" rule-node-config.save-time-series.persistence-settings
aria-hidden="false" </div>
aria-label="help-icon" <tb-toggle-select appearance="fill" selectMediaBreakpoint="xs"
matSuffix formControlName="isAdvanced">
matTooltip="{{ 'rule-node-config.default-ttl-hint' | translate }}"> <tb-toggle-option [value]=false>{{ 'rule-node-config.basic-mode' | translate}}</tb-toggle-option>
help <tb-toggle-option [value]=true>{{ 'rule-node-config.advanced-mode' | translate }}</tb-toggle-option>
</mat-icon> </tb-toggle-select>
<mat-error *ngIf="timeseriesConfigForm.get('defaultTTL').hasError('required')">
{{ 'rule-node-config.default-ttl-required' | translate }}
</mat-error>
<mat-error *ngIf="timeseriesConfigForm.get('defaultTTL').hasError('min')">
{{ 'rule-node-config.min-default-ttl-message' | translate }}
</mat-error>
</mat-form-field>
<div class="tb-form-panel stroked">
<div tb-hint-tooltip-icon="{{ 'rule-node-config.use-server-ts-hint' | translate}}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" formControlName="useServerTs">
{{ 'rule-node-config.use-server-ts' | translate }}
</mat-slide-toggle>
</div>
<div tb-hint-tooltip-icon="{{ 'rule-node-config.skip-latest-persistence-hint' | translate}}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" formControlName="skipLatestPersistence">
{{ 'rule-node-config.skip-latest-persistence' | translate }}
</mat-slide-toggle>
</div> </div>
@if(!timeseriesConfigForm.get('persistenceSettings.isAdvanced').value) {
<mat-form-field>
<mat-label translate>rule-node-config.save-time-series.strategy</mat-label>
<mat-select formControlName="type">
@for (strategy of persistenceStrategies; track strategy) {
<mat-option [value]="strategy">{{ PersistenceTypeTranslationMap.get(strategy) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
@if(timeseriesConfigForm.get('persistenceSettings.type').value === PersistenceType.DEDUPLICATE) {
<tb-time-unit-input
required
labelText="{{ 'rule-node-config.save-time-series.deduplication-interval' | translate }}"
requiredText="{{ 'rule-node-config.save-time-series.deduplication-interval-required' | translate }}"
minErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
maxErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
[maxTime]="maxDeduplicateTime"
formControlName="deduplicationIntervalSecs">
</tb-time-unit-input>
}
}
@else {
<tb-advanced-persistence-settings
class="mb-4"
formControlName="advanced"
></tb-advanced-persistence-settings>
}
</div> </div>
<section class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title translate>rule-node-config.advanced-settings</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-time-unit-input
required
labelText="{{ 'rule-node-config.default-ttl' | translate }}"
requiredText="{{ 'rule-node-config.default-ttl-required' | translate }}"
minErrorText="{{ 'rule-node-config.min-default-ttl-message' | translate }}"
formControlName="defaultTTL">
<mat-icon class="mr-2 cursor-pointer"
aria-hidden="false"
aria-label="help-icon"
matSuffix
matTooltip="{{ 'rule-node-config.default-ttl-hint' | translate }}">
help
</mat-icon>
</tb-time-unit-input>
<div tb-hint-tooltip-icon="{{ 'rule-node-config.use-server-ts-hint' | translate}}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" formControlName="useServerTs">
{{ 'rule-node-config.use-server-ts' | translate }}
</mat-slide-toggle>
</div>
</ng-template>
</mat-expansion-panel>
</section>
</section> </section>

105
ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts

@ -15,8 +15,18 @@
/// ///
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; import { RuleNodeConfigurationComponent } from '@shared/models/rule-node.models';
import {
defaultAdvancedPersistenceStrategy,
maxDeduplicateTimeSecs,
PersistenceSettings,
PersistenceSettingsForm,
PersistenceType,
PersistenceTypeTranslationMap,
TimeseriesNodeConfiguration,
TimeseriesNodeConfigurationForm
} from '@home/components/rule-node/action/timeseries-config.models';
@Component({ @Component({
selector: 'tb-action-node-timeseries-config', selector: 'tb-action-node-timeseries-config',
@ -25,21 +35,98 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m
}) })
export class TimeseriesConfigComponent extends RuleNodeConfigurationComponent { export class TimeseriesConfigComponent extends RuleNodeConfigurationComponent {
timeseriesConfigForm: UntypedFormGroup; timeseriesConfigForm: FormGroup;
constructor(private fb: UntypedFormBuilder) { PersistenceType = PersistenceType;
persistenceStrategies = [PersistenceType.ON_EVERY_MESSAGE, PersistenceType.DEDUPLICATE, PersistenceType.WEBSOCKETS_ONLY];
PersistenceTypeTranslationMap = PersistenceTypeTranslationMap;
maxDeduplicateTime = maxDeduplicateTimeSecs
constructor(private fb: FormBuilder) {
super(); super();
} }
protected configForm(): UntypedFormGroup { protected configForm(): FormGroup {
return this.timeseriesConfigForm; return this.timeseriesConfigForm;
} }
protected onConfigurationSet(configuration: RuleNodeConfiguration) { protected validatorTriggers(): string[] {
return ['persistenceSettings.isAdvanced', 'persistenceSettings.type'];
}
protected prepareInputConfig(config: TimeseriesNodeConfiguration): TimeseriesNodeConfigurationForm {
let persistenceSettings: PersistenceSettingsForm;
if (config?.persistenceSettings) {
const isAdvanced = config?.persistenceSettings?.type === PersistenceType.ADVANCED;
persistenceSettings = {
...config.persistenceSettings,
isAdvanced: isAdvanced,
type: isAdvanced ? PersistenceType.ON_EVERY_MESSAGE : config.persistenceSettings.type,
advanced: isAdvanced ? config.persistenceSettings : defaultAdvancedPersistenceStrategy
}
} else {
persistenceSettings = {
type: PersistenceType.ON_EVERY_MESSAGE,
isAdvanced: false,
deduplicationIntervalSecs: 10,
advanced: defaultAdvancedPersistenceStrategy
};
}
return {
...config,
persistenceSettings: persistenceSettings
}
}
protected prepareOutputConfig(config: TimeseriesNodeConfigurationForm): TimeseriesNodeConfiguration {
let persistenceSettings: PersistenceSettings;
if (config.persistenceSettings.isAdvanced) {
persistenceSettings = {
...config.persistenceSettings.advanced,
type: PersistenceType.ADVANCED
};
} else {
persistenceSettings = {
type: config.persistenceSettings.type,
deduplicationIntervalSecs: config.persistenceSettings?.deduplicationIntervalSecs
};
}
return {
...config,
persistenceSettings
};
}
protected onConfigurationSet(config: TimeseriesNodeConfigurationForm) {
this.timeseriesConfigForm = this.fb.group({ this.timeseriesConfigForm = this.fb.group({
defaultTTL: [configuration ? configuration.defaultTTL : null, [Validators.required, Validators.min(0)]], persistenceSettings: this.fb.group({
skipLatestPersistence: [configuration ? configuration.skipLatestPersistence : false, []], isAdvanced: [config?.persistenceSettings?.isAdvanced ?? false],
useServerTs: [configuration ? configuration.useServerTs : false, []] type: [config?.persistenceSettings?.type ?? PersistenceType.ON_EVERY_MESSAGE],
deduplicationIntervalSecs: [
{value: config?.persistenceSettings?.deduplicationIntervalSecs ?? 10, disabled: true},
[Validators.required, Validators.max(maxDeduplicateTimeSecs)]
],
advanced: [{value: null, disabled: true}]
}),
defaultTTL: [config?.defaultTTL ?? null, [Validators.required, Validators.min(0)]],
useServerTs: [config?.useServerTs ?? false]
}); });
} }
protected updateValidators(emitEvent: boolean, _trigger?: string) {
const persistenceForm = this.timeseriesConfigForm.get('persistenceSettings') as FormGroup;
const isAdvanced: boolean = persistenceForm.get('isAdvanced').value;
const type: PersistenceType = persistenceForm.get('type').value;
if (!isAdvanced && type === PersistenceType.DEDUPLICATE) {
persistenceForm.get('deduplicationIntervalSecs').enable({emitEvent});
} else {
persistenceForm.get('deduplicationIntervalSecs').disable({emitEvent});
}
if (isAdvanced) {
persistenceForm.get('advanced').enable({emitEvent});
} else {
persistenceForm.get('advanced').disable({emitEvent});
}
}
} }

78
ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.models.ts

@ -0,0 +1,78 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { DAY, SECOND } from '@shared/models/time/time.models';
export const maxDeduplicateTimeSecs = DAY / SECOND + 1;
export interface TimeseriesNodeConfiguration {
persistenceSettings: PersistenceSettings;
defaultTTL: number;
useServerTs: boolean;
}
export interface TimeseriesNodeConfigurationForm extends Omit<TimeseriesNodeConfiguration, 'persistenceSettings'> {
persistenceSettings: PersistenceSettingsForm
}
export type PersistenceSettings = BasicPersistenceSettings & Partial<DeduplicatePersistenceStrategy> & Partial<AdvancedPersistenceStrategy>;
export type PersistenceSettingsForm = Omit<PersistenceSettings, keyof AdvancedPersistenceStrategy> & {
isAdvanced: boolean;
advanced?: Partial<AdvancedPersistenceStrategy>;
type: PersistenceType;
};
export enum PersistenceType {
ON_EVERY_MESSAGE = 'ON_EVERY_MESSAGE',
DEDUPLICATE = 'DEDUPLICATE',
WEBSOCKETS_ONLY = 'WEBSOCKETS_ONLY',
ADVANCED = 'ADVANCED',
SKIP = 'SKIP'
}
export const PersistenceTypeTranslationMap = new Map<PersistenceType, string>([
[PersistenceType.ON_EVERY_MESSAGE, 'rule-node-config.save-time-series.strategy-type.every-message'],
[PersistenceType.DEDUPLICATE, 'rule-node-config.save-time-series.strategy-type.deduplicate'],
[PersistenceType.WEBSOCKETS_ONLY, 'rule-node-config.save-time-series.strategy-type.web-sockets-only'],
[PersistenceType.SKIP, 'rule-node-config.save-time-series.strategy-type.skip'],
])
export interface BasicPersistenceSettings {
type: PersistenceType;
}
export interface DeduplicatePersistenceStrategy extends BasicPersistenceSettings{
deduplicationIntervalSecs: number;
}
export interface AdvancedPersistenceStrategy extends BasicPersistenceSettings{
timeseries: AdvancedPersistenceConfig;
latest: AdvancedPersistenceConfig;
webSockets: AdvancedPersistenceConfig;
}
export type AdvancedPersistenceConfig = WithOptional<DeduplicatePersistenceStrategy, 'deduplicationIntervalSecs'>;
export const defaultAdvancedPersistenceConfig: AdvancedPersistenceConfig = {
type: PersistenceType.ON_EVERY_MESSAGE
}
export const defaultAdvancedPersistenceStrategy: Omit<AdvancedPersistenceStrategy, 'type'> = {
timeseries: defaultAdvancedPersistenceConfig,
latest: defaultAdvancedPersistenceConfig,
webSockets: defaultAdvancedPersistenceConfig,
}

7
ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts

@ -33,6 +33,7 @@ import { RelationsQueryConfigOldComponent } from './relations-query-config-old.c
import { SelectAttributesComponent } from './select-attributes.component'; import { SelectAttributesComponent } from './select-attributes.component';
import { AlarmStatusSelectComponent } from './alarm-status-select.component'; import { AlarmStatusSelectComponent } from './alarm-status-select.component';
import { ExampleHintComponent } from './example-hint.component'; import { ExampleHintComponent } from './example-hint.component';
import { TimeUnitInputComponent } from './time-unit-input.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -50,7 +51,8 @@ import { ExampleHintComponent } from './example-hint.component';
RelationsQueryConfigOldComponent, RelationsQueryConfigOldComponent,
SelectAttributesComponent, SelectAttributesComponent,
AlarmStatusSelectComponent, AlarmStatusSelectComponent,
ExampleHintComponent ExampleHintComponent,
TimeUnitInputComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -72,7 +74,8 @@ import { ExampleHintComponent } from './example-hint.component';
RelationsQueryConfigOldComponent, RelationsQueryConfigOldComponent,
SelectAttributesComponent, SelectAttributesComponent,
AlarmStatusSelectComponent, AlarmStatusSelectComponent,
ExampleHintComponent ExampleHintComponent,
TimeUnitInputComponent
] ]
}) })

44
ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="timeInputForm" class="flex gap-4">
<mat-form-field class="max-w-66% flex-full" subscriptSizing="dynamic">
<mat-label *ngIf="labelText">{{ labelText }}</mat-label>
<input type="number" min="0" step="1" matInput formControlName="time">
<div matSuffix>
<ng-content select="[matSuffix]"></ng-content>
</div>
<mat-hint></mat-hint>
<mat-error *ngIf="timeInputForm.get('time').hasError('required') && requiredText">
{{ requiredText }}
</mat-error>
<mat-error *ngIf="timeInputForm.get('time').hasError('min') && minErrorText">
{{ minErrorText }}
</mat-error>
<mat-error *ngIf="timeInputForm.get('time').hasError('max') && maxErrorText">
{{ maxErrorText }}
</mat-error>
</mat-form-field>
<mat-form-field class="max-w-33% flex-full">
<mat-label translate>rule-node-config.units</mat-label>
<mat-select formControlName="timeUnit">
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">{{ timeUnitTranslations.get(timeUnit) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</section>

184
ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts

@ -0,0 +1,184 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator, Validators
} from '@angular/forms';
import { TimeUnit, timeUnitTranslations } from '../rule-node-config.models';
import { isDefinedAndNotNull, isNumeric } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models';
interface TimeUnitInputModel {
time: number;
timeUnit: TimeUnit
}
@Component({
selector: 'tb-time-unit-input',
templateUrl: './time-unit-input.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeUnitInputComponent),
multi: true
},{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TimeUnitInputComponent),
multi: true
}]
})
export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit {
@Input()
labelText: string;
@Input()
@coerceBoolean()
required: boolean;
@Input()
requiredText: string;
@Input()
minErrorText: string;
@Input()
@coerceNumber()
maxTime: number;
@Input()
maxErrorText: string;
timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
timeUnitTranslations = timeUnitTranslations;
timeInputForm = this.fb.group({
time: [0, Validators.min(0)],
timeUnit: [TimeUnit.SECONDS]
});
private timeIntervalsInSec = new Map<TimeUnit, number>([
[TimeUnit.DAYS, DAY/SECOND],
[TimeUnit.HOURS, HOUR/SECOND],
[TimeUnit.MINUTES, MINUTE/SECOND],
[TimeUnit.SECONDS, SECOND/SECOND],
]);
private modelValue: number;
private propagateChange: (value: any) => void = () => {};
constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit() {
if(this.required || this.maxTime) {
const timeControl = this.timeInputForm.get('time');
const validators = [];
if (this.required) {
validators.push(Validators.required);
}
if (this.maxTime) {
validators.push((control: AbstractControl) =>
Validators.max(Math.floor(this.maxTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)))(control)
);
}
timeControl.setValidators(validators);
timeControl.updateValueAndValidity({ emitEvent: false });
}
this.timeInputForm.get('timeUnit').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.timeInputForm.get('time').updateValueAndValidity({onlySelf: true});
this.timeInputForm.get('time').markAsTouched({onlySelf: true});
});
this.timeInputForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
this.updatedModel(value);
});
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(_fn: any) {
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.timeInputForm.disable({emitEvent: false});
} else {
this.timeInputForm.enable({emitEvent: false});
}
}
writeValue(sec: number) {
if (sec !== this.modelValue) {
if (isDefinedAndNotNull(sec) && isNumeric(sec) && Number(sec) !== 0) {
this.timeInputForm.patchValue(this.parseTime(sec), {emitEvent: false});
this.modelValue = sec;
} else {
this.timeInputForm.patchValue({
time: 0,
timeUnit: TimeUnit.SECONDS
}, {emitEvent: false});
this.modelValue = 0;
}
}
}
validate(): ValidationErrors | null {
return this.timeInputForm.valid ? null : {
timeInput: false
};
}
private updatedModel(value: Partial<TimeUnitInputModel>) {
const time = value.time * this.timeIntervalsInSec.get(value.timeUnit);
if (this.modelValue !== time) {
this.modelValue = time;
this.propagateChange(time);
}
}
private parseTime(value: number): TimeUnitInputModel {
for (const [timeUnit, timeValue] of this.timeIntervalsInSec) {
const calc = value / timeValue;
if (Number.isInteger(calc)) {
return {
time: calc,
timeUnit: timeUnit
}
}
}
}
}

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

@ -4544,7 +4544,7 @@
"originator-entity": "Entity by name pattern", "originator-entity": "Entity by name pattern",
"clone-message": "Clone message", "clone-message": "Clone message",
"transform": "Transform", "transform": "Transform",
"default-ttl": "Default TTL in seconds", "default-ttl": "Default TTL",
"default-ttl-required": "Default TTL is required.", "default-ttl-required": "Default TTL is required.",
"default-ttl-hint": "Rule node will fetch Time-to-Live (TTL) value from the message metadata. If no value is present, it defaults to the TTL specified in the configuration. If the value is set to 0, the TTL from the tenant profile configuration will be applied.", "default-ttl-hint": "Rule node will fetch Time-to-Live (TTL) value from the message metadata. If no value is present, it defaults to the TTL specified in the configuration. If the value is set to 0, the TTL from the tenant profile configuration will be applied.",
"default-ttl-zero-hint": "TTL will not be applied if its value is set to 0.", "default-ttl-zero-hint": "TTL will not be applied if its value is set to 0.",
@ -4909,10 +4909,8 @@
"general-pattern-hint": "Use ${metadataKey} for value from metadata, $[messageKey] for value from message body.", "general-pattern-hint": "Use ${metadataKey} for value from metadata, $[messageKey] for value from message body.",
"alarm-severity-pattern-hint": "Use <code><span style=\"color: #000;\">$&#123;</span>metadataKey<span style=\"color: #000;\">&#125;</span></code> for value from metadata, <code><span style=\"color: #000;\">$[</span>messageKey<span style=\"color: #000;\">]</span></code> for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)", "alarm-severity-pattern-hint": "Use <code><span style=\"color: #000;\">$&#123;</span>metadataKey<span style=\"color: #000;\">&#125;</span></code> for value from metadata, <code><span style=\"color: #000;\">$[</span>messageKey<span style=\"color: #000;\">]</span></code> for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)",
"output-node-name-hint": "The <b>rule node name</b> corresponds to the <b>relation type</b> of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.", "output-node-name-hint": "The <b>rule node name</b> corresponds to the <b>relation type</b> of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.",
"skip-latest-persistence": "Skip latest persistence", "use-server-ts": "Use server timestamp",
"skip-latest-persistence-hint": "Rule node will not update values for incoming keys for the latest time series data. Useful for highly loaded use-cases to decrease the pressure on the DB.", "use-server-ts-hint": "Use the server’s current timestamp for time series data that lacks an explicit timestamp. This helps maintain proper ordering when processing messages from multiple sources or when messages arrive out of sequence.",
"use-server-ts": "Use server ts",
"use-server-ts-hint": "Rule node will use the timestamp of message processing instead of the timestamp from the message. Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).",
"kv-map-pattern-hint": "All input fields support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.", "kv-map-pattern-hint": "All input fields support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.",
"kv-map-single-pattern-hint": "Input field support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.", "kv-map-single-pattern-hint": "Input field support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.",
"shared-scope": "Shared scope", "shared-scope": "Shared scope",
@ -5070,8 +5068,28 @@
"request-timeout-required": "Request timeout is required", "request-timeout-required": "Request timeout is required",
"request-timeout-min": "Min request timeout is 0", "request-timeout-min": "Min request timeout is 0",
"request-timeout-hint": "The amount of time to wait in seconds for the request to complete before giving up and timing out. A value of 0 means infinity, and is not recommended.", "request-timeout-hint": "The amount of time to wait in seconds for the request to complete before giving up and timing out. A value of 0 means infinity, and is not recommended.",
"units": "Units",
"tell-failure-aws-lambda": "Tell Failure if AWS Lambda function execution raises exception", "tell-failure-aws-lambda": "Tell Failure if AWS Lambda function execution raises exception",
"tell-failure-aws-lambda-hint": "Rule node forces failure of message processing if AWS Lambda function execution raises exception.", "tell-failure-aws-lambda-hint": "Rule node forces failure of message processing if AWS Lambda function execution raises exception.",
"basic-mode": "Basic",
"advanced-mode": "Advanced",
"save-time-series": {
"persistence-settings": "Persistence settings",
"persistence-settings-hint": "Define how and when time series data is saved. In Basic mode, apply a single persistence strategy to all actions or enable only WebSockets updates. Advanced mode allows you to configure individual persistence strategies for each action.",
"strategy": "Strategy",
"deduplication-interval": "Deduplication interval",
"deduplication-interval-required": "Deduplication interval is required",
"deduplication-interval-min-max-range": "Deduplication interval should be at least 1 second and at most 1 day",
"strategy-type": {
"every-message": "On every message",
"skip": "Skip",
"deduplicate": "Deduplicate",
"web-sockets-only": "WebSockets only"
},
"time-series": "Time series",
"latest": "Latest",
"web-sockets": "WebSockets"
},
"key-val": { "key-val": {
"key": "Key", "key": "Key",
"value": "Value", "value": "Value",

Loading…
Cancel
Save