Fix device profile provision configuration conflict

This commit is contained in:
Andrii Landiak 2023-03-29 11:50:27 +03:00
commit 82f2915865
93 changed files with 921 additions and 887 deletions

View File

@ -582,12 +582,9 @@ export class WidgetSubscription implements IWidgetSubscription {
const data = this.alarms.data[0];
entityId = data.originator;
entityName = data.originatorName;
entityLabel = data.originatorLabel;
if (data.latest && data.latest[EntityKeyType.ENTITY_FIELD]) {
const entityFields = data.latest[EntityKeyType.ENTITY_FIELD];
const labelValue = entityFields.label;
if (labelValue) {
entityLabel = labelValue.value;
}
const additionalInfoValue = entityFields.additionalInfo;
if (additionalInfoValue) {
const additionalInfo = additionalInfoValue.value;

View File

@ -29,7 +29,11 @@ import {
isDefined,
isDefinedAndNotNull,
isString,
isUndefined
isUndefined,
objToBase64,
objToBase64URI,
base64toString,
base64toObj
} from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core';
@ -87,7 +91,7 @@ const commonMaterialIcons: Array<string> = ['more_horiz', 'more_vert', 'open_in_
'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
'share', 'add', 'edit', 'done'];
'share', 'add', 'edit', 'done', 'delete'];
// @dynamic
@Injectable({
@ -518,4 +522,21 @@ export class UtilsService {
refCount()
);
}
public objToBase64(obj: any): string {
return objToBase64(obj);
}
public base64toString(b64Encoded: string): string {
return base64toString(b64Encoded);
}
public objToBase64URI(obj: any): string {
return objToBase64URI(obj);
}
public base64toObj(b64Encoded: string): any {
return base64toObj(b64Encoded);
}
}

View File

@ -42,7 +42,7 @@
<div class="predicate-list">
<div fxLayout="row" fxLayoutAlign="start" style="height: 71px;"
formArrayName="predicates"
*ngFor="let predicateControl of predicatesFormArray().controls; let $index = index">
*ngFor="let predicateControl of predicatesFormArray.controls; let $index = index">
<div fxFlex="8" fxLayout="row" fxLayoutAlign="center" class="filters-operation">
<span *ngIf="$index > 0">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
@ -68,7 +68,7 @@
</div>
</div>
</div>
<span [fxShow]="!predicatesFormArray().length"
<span [fxShow]="!predicatesFormArray.length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-filters</span>
</div>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Inject, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -27,7 +27,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs';
import { Observable, of, Subject } from 'rxjs';
import {
ComplexFilterPredicateInfo,
ComplexOperation,
@ -37,7 +37,7 @@ import {
KeyFilterPredicateInfo
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { map } from 'rxjs/operators';
import { map, takeUntil } from 'rxjs/operators';
import { ComponentType } from '@angular/cdk/portal';
import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/components/tokens';
import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models';
@ -59,7 +59,7 @@ import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter
}
]
})
export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit {
export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@Input() disabled: boolean;
@ -81,22 +81,29 @@ export class FilterPredicateListComponent implements ControlValueAccessor, Valid
complexOperationTranslations = complexOperationTranslationMap;
private destroy$ = new Subject<void>();
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(private fb: UntypedFormBuilder,
@Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType<any>,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.filterListFormGroup = this.fb.group({});
this.filterListFormGroup.addControl('predicates',
this.fb.array([]));
this.filterListFormGroup = this.fb.group({
predicates: this.fb.array([])
});
this.filterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
predicatesFormArray(): UntypedFormArray {
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get predicatesFormArray(): UntypedFormArray {
return this.filterListFormGroup.get('predicates') as UntypedFormArray;
}
@ -123,28 +130,26 @@ export class FilterPredicateListComponent implements ControlValueAccessor, Valid
}
writeValue(predicates: Array<KeyFilterPredicateInfo>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const predicateControls: Array<AbstractControl> = [];
if (predicates) {
for (const predicate of predicates) {
predicateControls.push(this.fb.control(predicate, [Validators.required]));
}
}
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls));
this.valueChangeSubscription = this.filterListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
if (predicates.length === this.predicatesFormArray.length) {
this.predicatesFormArray.patchValue(predicates, {emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
const predicateControls: Array<AbstractControl> = [];
if (predicates) {
for (const predicate of predicates) {
predicateControls.push(this.fb.control(predicate, [Validators.required]));
}
}
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls), {emitEvent: false});
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
}
}
}
public removePredicate(index: number) {
(this.filterListFormGroup.get('predicates') as UntypedFormArray).removeAt(index);
this.predicatesFormArray.removeAt(index);
}
public addPredicate(complex: boolean) {

View File

@ -64,6 +64,15 @@
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 48px;"
type="button"
(click)="duplicateFilter($index)"
matTooltip="{{ 'filter.duplicate-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>content_copy</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"

View File

@ -64,6 +64,8 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
filterToWidgetsMap: {[filterId: string]: Array<string>} = {};
filterNames: Set<string> = new Set<string>();
filtersFormGroup: UntypedFormGroup;
submitted = false;
@ -112,6 +114,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
if (isUndefined(filter.editable)) {
filter.editable = true;
}
this.filterNames.add(filter.filter);
filterControls.push(this.createFilterFormControl(filterId, filter));
}
@ -158,10 +161,37 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
message, this.translate.instant('action.close'), true);
} else {
(this.filtersFormGroup.get('filters') as UntypedFormArray).removeAt(index);
this.filterNames.delete(filter.filter);
this.filtersFormGroup.markAsDirty();
}
}
private getNextDuplicatedName(filterName: string): string {
const suffix = ` - ${this.translate.instant('action.copy')} `;
let counter = 0;
while (++counter < Number.MAX_SAFE_INTEGER) {
const newName = `${filterName}${suffix}${counter}`;
if (!this.filterNames.has(newName)) {
return newName;
}
}
return null;
}
duplicateFilter(index: number) {
const originalFilter = (this.filtersFormGroup.get('filters').value as any[])[index];
const newFilterName = this.getNextDuplicatedName(originalFilter.filter);
if (newFilterName) {
const duplicatedFilter = deepClone(originalFilter);
duplicatedFilter.id = this.utils.guid();
duplicatedFilter.filter = newFilterName;
(this.filtersFormGroup.get('filters') as UntypedFormArray).
insert(index + 1, this.createFilterFormControl(duplicatedFilter.id, duplicatedFilter));
this.filterNames.add(duplicatedFilter.filter);
}
}
public addFilter() {
this.openFilterDialog(-1);
}
@ -176,6 +206,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
const filtersArray = this.filtersFormGroup.get('filters').value as any[];
if (!isAdd) {
filter = filtersArray[index];
this.filterNames.delete(filter.filter);
}
this.dialog.open<FilterDialogComponent, FilterDialogData,
Filter>(FilterDialogComponent, {
@ -197,6 +228,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
filterFormControl.get('editable').patchValue(result.editable);
filterFormControl.get('keyFilters').patchValue(result.keyFilters);
}
this.filterNames.add(result.filter);
this.filtersFormGroup.markAsDirty();
}
});

View File

@ -36,7 +36,7 @@
<div class="key-filter-list">
<div fxLayout="row" fxLayoutAlign="start" style="max-height: 71px;"
formArrayName="keyFilters"
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
*ngFor="let keyFilterControl of keyFiltersFormArray.controls; let $index = index">
<div fxFlex="8" class="filters-operation">
<span *ngIf="$index > 0" translate>filter.operation.and</span>
</div>
@ -63,7 +63,7 @@
<mat-divider></mat-divider>
</div>
</div>
<span [fxShow]="!keyFiltersFormArray().length"
<span [fxShow]="!keyFiltersFormArray.length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-key-filters</span>
</div>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -28,7 +28,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import {
EntityKeyType,
entityKeyTypeTranslationMap,
@ -39,6 +39,7 @@ import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils';
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-key-filter-list',
@ -57,7 +58,7 @@ import { EntityId } from '@shared/models/id/entity-id';
}
]
})
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit {
export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@Input() disabled: boolean;
@ -75,22 +76,30 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
keyFiltersControl: UntypedFormControl;
private destroy$ = new Subject<void>();
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.keyFilterListFormGroup = this.fb.group({});
this.keyFilterListFormGroup.addControl('keyFilters',
this.fb.array([]));
this.keyFilterListFormGroup = this.fb.group({
keyFilters: this.fb.array([])
});
this.keyFiltersControl = this.fb.control(null);
this.keyFilterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
keyFiltersFormArray(): UntypedFormArray {
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get keyFiltersFormArray(): UntypedFormArray {
return this.keyFilterListFormGroup.get('keyFilters') as UntypedFormArray;
}
@ -119,23 +128,21 @@ export class KeyFilterListComponent implements ControlValueAccessor, Validator,
}
writeValue(keyFilters: Array<KeyFilterInfo>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const keyFilterControls: Array<AbstractControl> = [];
if (keyFilters) {
for (const keyFilter of keyFilters) {
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required]));
}
}
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls));
this.valueChangeSubscription = this.keyFilterListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false});
if (keyFilters.length === this.keyFiltersFormArray.length) {
this.keyFiltersFormArray.patchValue(keyFilters, {emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
const keyFilterControls: Array<AbstractControl> = [];
if (keyFilters) {
for (const keyFilter of keyFilters) {
keyFilterControls.push(this.fb.control(keyFilter, [Validators.required]));
}
}
this.keyFilterListFormGroup.setControl('keyFilters', this.fb.array(keyFilterControls), {emitEvent: false});
if (this.disabled) {
this.keyFilterListFormGroup.disable({emitEvent: false});
} else {
this.keyFilterListFormGroup.enable({emitEvent: false});
}
}
const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false});

View File

@ -15,124 +15,128 @@
limitations under the License.
-->
<div>
<mat-toolbar color="primary">
<h2 translate>device-profile.add</h2>
<span fxFlex></span>
<div [tb-help]="'deviceProfiles'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" #addDeviceProfileStepper (selectionChange)="changeStep($event)">
<mat-step [stepControl]="deviceProfileDetailsFormGroup">
<form [formGroup]="deviceProfileDetailsFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-profile-details' | translate }}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('required')">
{{ 'device-profile.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('maxlength')">
{{ 'device-profile.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-rule-chain-autocomplete
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete
label="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId">
<span tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</span>
</tb-dashboard-autocomplete>
<tb-queue-autocomplete
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-rule-chain-autocomplete
labelText="device-profile.default-edge-rule-chain"
formControlName="defaultEdgeRuleChainId"
[ruleChainType]="edgeRuleChainType">
<span tb-hint>{{'device-profile.default-edge-rule-chain-hint' | translate}}</span>
</tb-rule-chain-autocomplete>
<mat-form-field fxHide class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
{{deviceProfileTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('type').hasError('required')">
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-image-input fxFlex
label="{{'device-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
<mat-toolbar color="primary">
<h2 translate>device-profile.add</h2>
<span fxFlex></span>
<div [tb-help]="'deviceProfiles'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true"
#addDeviceProfileStepper
[labelPosition]="(stepperLabelPosition | async)"
[orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)">
<mat-step [stepControl]="deviceProfileDetailsFormGroup">
<form [formGroup]="deviceProfileDetailsFormGroup">
<ng-template matStepLabel>{{ 'device-profile.device-profile-details' | translate }}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
<mat-label translate>device-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('required')">
{{ 'device-profile.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('maxlength')">
{{ 'device-profile.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="row">
<tb-rule-chain-autocomplete
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete
label="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId">
<span tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</span>
</tb-dashboard-autocomplete>
<tb-queue-autocomplete
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
<tb-rule-chain-autocomplete
labelText="device-profile.default-edge-rule-chain"
formControlName="defaultEdgeRuleChainId"
[ruleChainType]="edgeRuleChainType">
<span tb-hint>{{'device-profile.default-edge-rule-chain-hint' | translate}}</span>
</tb-rule-chain-autocomplete>
<mat-form-field fxHide class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
{{deviceProfileTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('type').hasError('required')">
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-image-input fxFlex
label="{{'device-profile.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions style="padding: 0">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center">
<button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
@ -143,8 +147,8 @@
[disabled]="(isLoading$ | async)"
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<mat-divider style="width: 100%"></mat-divider>
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end center">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>

View File

@ -15,6 +15,15 @@
*/
@import "../../../../../scss/constants";
:host {
height: 100%;
display: grid;
.dialog-actions-row {
padding: 8px;
}
}
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) {
@media #{$mat-lt-sm} {
.mat-mdc-dialog-content {
@ -32,31 +41,21 @@
.mat-stepper-horizontal {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
@media #{$mat-lt-sm} {
.mat-step-label {
white-space: normal;
overflow: visible;
.mat-step-text-label {
overflow: visible;
}
}
.mat-horizontal-stepper-wrapper {
flex: 1 1 100%;
}
.mat-horizontal-content-container {
height: 530px;
height: 680px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
scrollbar-gutter: stable;
@media #{$mat-gt-sm} {
min-width: 800px;
}
}
.mat-horizontal-stepper-content[aria-expanded=true] {
height: 100%;
form {
height: 100%;
min-width: 500px;
}
}
}

View File

@ -44,13 +44,17 @@ import {
} from '@shared/models/device.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { EntityType } from '@shared/models/entity-type.models';
import { MatStepper } from '@angular/material/stepper';
import { MatStepper, StepperOrientation } from '@angular/material/stepper';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { deepTrim } from '@core/utils';
import { ServiceType } from '@shared/models/queue.models';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { RuleChainType } from '@shared/models/rule-chain.models';
import { Observable } from 'rxjs';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { map } from 'rxjs/operators';
export interface AddDeviceProfileDialogData {
deviceProfileName: string;
@ -67,7 +71,8 @@ export class AddDeviceProfileDialogComponent extends
DialogComponent<AddDeviceProfileDialogComponent, DeviceProfile> implements AfterViewInit {
@ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatStepper;
stepperOrientation: Observable<StepperOrientation>;
stepperLabelPosition: Observable<'bottom' | 'end'>;
selectedIndex = 0;
showNext = true;
@ -102,10 +107,17 @@ export class AddDeviceProfileDialogComponent extends
public dialogRef: MatDialogRef<AddDeviceProfileDialogComponent, DeviceProfile>,
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
private breakpointObserver: BreakpointObserver,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private deviceProfileService: DeviceProfileService,
private fb: UntypedFormBuilder) {
super(store, router, dialogRef);
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
this.stepperLabelPosition = this.breakpointObserver.observe(MediaBreakpoints['gt-md'])
.pipe(map(({matches}) => matches ? 'end' : 'bottom'));
this.deviceProfileDetailsFormGroup = this.fb.group(
{
name: [data.deviceProfileName, [Validators.required, Validators.maxLength(255)]],

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -23,15 +23,16 @@ import {
UntypedFormControl,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, ValidationErrors,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { DeviceProfileAlarmRule, alarmRuleValidator } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-create-alarm-rules',
@ -50,7 +51,7 @@ import { EntityId } from '@shared/models/id/entity-id';
}
]
})
export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator {
export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
@ -67,8 +68,7 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
private usedSeverities: AlarmSeverity[] = [];
private valueChangeSubscription: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
@ -86,6 +86,14 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
this.createAlarmRulesFormGroup = this.fb.group({
createAlarmRules: this.fb.array([])
});
this.createAlarmRulesFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
createAlarmRulesFormArray(): UntypedFormArray {
@ -102,9 +110,6 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
}
writeValue(createAlarmRules: {[severity: string]: DeviceProfileAlarmRule}): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const createAlarmRulesControls: Array<AbstractControl> = [];
if (createAlarmRules) {
Object.keys(createAlarmRules).forEach((severity) => {
@ -118,15 +123,12 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
}));
});
}
this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls));
this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls), {emitEvent: false});
if (this.disabled) {
this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else {
this.createAlarmRulesFormGroup.enable({emitEvent: false});
}
this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedSeverities();
if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
this.updateModel();

View File

@ -59,10 +59,10 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
<mat-checkbox formControlName="propagate" style="display: block;">
{{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox>
<section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">
<section *ngIf="alarmFormGroup.get('propagate').value === true">
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList [disabled]="disabled">
@ -82,10 +82,10 @@
<mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
</section>
<mat-checkbox formControlName="propagateToOwner" style="display: block; padding-bottom: 16px;">
<mat-checkbox formControlName="propagateToOwner" style="display: block;">
{{ 'device-profile.propagate-alarm-to-owner' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="propagateToTenant" style="display: block; padding-bottom: 16px;">
<mat-checkbox formControlName="propagateToTenant" style="display: block;">
{{ 'device-profile.propagate-alarm-to-tenant' | translate }}
</mat-checkbox>
</ng-template>

View File

@ -17,7 +17,7 @@
-->
<div fxLayout="column">
<div class="tb-device-profile-alarms">
<div *ngFor="let alarmControl of alarmsFormArray().controls; trackBy: trackByAlarm;
<div *ngFor="let alarmControl of alarmsFormArray.controls; trackBy: trackByAlarm;
let $index = index; last as isLast;"
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-device-profile-alarm [formControl]="alarmControl"
@ -27,7 +27,7 @@
</tb-device-profile-alarm>
</div>
</div>
<div *ngIf="!alarmsFormArray().controls.length">
<div *ngIf="!alarmsFormArray.controls.length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-alarm-rules</span>
</div>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -32,9 +32,9 @@ import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models';
import { guid } from '@core/utils';
import { Subscription } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-device-profile-alarms',
@ -53,7 +53,7 @@ import { EntityId } from '@shared/models/id/entity-id';
}
]
})
export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator {
export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
deviceProfileAlarmsFormGroup: UntypedFormGroup;
@ -72,13 +72,11 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
@Input()
deviceProfileId: EntityId;
private valueChangeSubscription: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private fb: UntypedFormBuilder,
private dialog: MatDialog) {
private fb: UntypedFormBuilder) {
}
registerOnChange(fn: any): void {
@ -92,9 +90,17 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
this.deviceProfileAlarmsFormGroup = this.fb.group({
alarms: this.fb.array([])
});
this.deviceProfileAlarmsFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
alarmsFormArray(): UntypedFormArray {
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get alarmsFormArray(): UntypedFormArray {
return this.deviceProfileAlarmsFormGroup.get('alarms') as UntypedFormArray;
}
@ -108,24 +114,22 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
}
writeValue(alarms: Array<DeviceProfileAlarm> | null): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const alarmsControls: Array<AbstractControl> = [];
if (alarms) {
alarms.forEach((alarm) => {
alarmsControls.push(this.fb.control(alarm, [Validators.required]));
});
}
this.deviceProfileAlarmsFormGroup.setControl('alarms', this.fb.array(alarmsControls));
if (this.disabled) {
this.deviceProfileAlarmsFormGroup.disable({emitEvent: false});
if (alarms?.length === this.alarmsFormArray.length) {
this.alarmsFormArray.patchValue(alarms, {emitEvent: false});
} else {
this.deviceProfileAlarmsFormGroup.enable({emitEvent: false});
const alarmsControls: Array<AbstractControl> = [];
if (alarms) {
alarms.forEach((alarm) => {
alarmsControls.push(this.fb.control(alarm, [Validators.required]));
});
}
this.deviceProfileAlarmsFormGroup.setControl('alarms', this.fb.array(alarmsControls), {emitEvent: false});
if (this.disabled) {
this.deviceProfileAlarmsFormGroup.disable({emitEvent: false});
} else {
this.deviceProfileAlarmsFormGroup.enable({emitEvent: false});
}
}
this.valueChangeSubscription = this.deviceProfileAlarmsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
public trackByAlarm(index: number, alarmControl: AbstractControl): string {

View File

@ -26,7 +26,7 @@ import {
Validator,
Validators
} from '@angular/forms';
import { Subject, Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import {
AttributeName,
AttributeNameTranslationMap,
@ -70,7 +70,6 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
attributesValueFormGroup: UntypedFormGroup;
private propagateChange = null;
private valueChange$: Subscription = null;
private destroy$ = new Subject<void>();
private usedAttributesName: AttributeName[] = [];
@ -80,6 +79,9 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
this.attributesValueFormGroup = this.fb.group({
attributesValue: this.fb.array([])
});
this.attributesValueFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnInit() {
@ -92,9 +94,6 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
@ -116,24 +115,18 @@ export class Lwm2mAttributesKeyListComponent extends PageComponent implements Co
}
writeValue(keyValMap: AttributesNameValueMap): void {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const attributesValueControls: Array<AbstractControl> = [];
if (keyValMap) {
(Object.keys(keyValMap) as AttributeName[]).forEach(name => {
attributesValueControls.push(this.createdFormGroup({name, value: keyValMap[name]}));
});
}
this.attributesValueFormGroup.setControl('attributesValue', this.fb.array(attributesValueControls));
this.attributesValueFormGroup.setControl('attributesValue', this.fb.array(attributesValueControls), {emitEvent: false});
if (this.disabled) {
this.attributesValueFormGroup.disable({emitEvent: false});
} else {
this.attributesValueFormGroup.enable({emitEvent: false});
}
this.valueChange$ = this.attributesValueFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedAttributesName();
}

View File

@ -17,7 +17,7 @@
-->
<div fxLayout="column">
<mat-accordion multi="true">
<div *ngFor="let serverConfig of serverConfigsFromArray().controls; trackBy: trackByParams; let $index = index;">
<div *ngFor="let serverConfig of serverConfigsFromArray.controls; trackBy: trackByParams; let $index = index;">
<tb-profile-lwm2m-device-config-server
[formControl]="serverConfig"
(removeServer)="removeServerConfig($event, $index)"
@ -25,7 +25,7 @@
</tb-profile-lwm2m-device-config-server>
</div>
</mat-accordion>
<div *ngIf="!serverConfigsFromArray().controls.length" style="margin:32px 0">
<div *ngIf="!serverConfigsFromArray.controls.length" style="margin:32px 0">
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.lwm2m.no-config-servers</span>
</div>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -24,13 +24,13 @@ import {
NG_VALIDATORS,
NG_VALUE_ACCESSOR
} from '@angular/forms';
import { of, Subscription } from 'rxjs';
import { of, Subject } from 'rxjs';
import { ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog';
import { Lwm2mBootstrapAddConfigServerDialogComponent } from '@home/components/profile/device/lwm2m/lwm2m-bootstrap-add-config-server-dialog.component';
import { mergeMap } from 'rxjs/operators';
import { mergeMap, takeUntil } from 'rxjs/operators';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models';
@ -50,7 +50,7 @@ import { Lwm2mSecurityType } from '@shared/models/lwm2m-security-config.models';
}
]
})
export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValueAccessor {
export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValueAccessor, OnDestroy {
bootstrapConfigServersFormGroup: UntypedFormGroup;
@ -72,8 +72,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
}
}
private valueChangeSubscription: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(public translate: TranslateService,
@ -94,9 +93,17 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
this.bootstrapConfigServersFormGroup = this.fb.group({
serverConfigs: this.fb.array([])
});
this.bootstrapConfigServersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
serverConfigsFromArray(): UntypedFormArray {
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get serverConfigsFromArray(): UntypedFormArray {
return this.bootstrapConfigServersFormGroup.get('serverConfigs') as UntypedFormArray;
}
@ -110,24 +117,22 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
}
writeValue(serverConfigs: Array<ServerSecurityConfig> | null): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const serverConfigsControls: Array<AbstractControl> = [];
if (serverConfigs) {
serverConfigs.forEach((serverConfig) => {
serverConfigsControls.push(this.fb.control(serverConfig));
});
}
this.bootstrapConfigServersFormGroup.setControl('serverConfigs', this.fb.array(serverConfigsControls));
if (this.disabled) {
this.bootstrapConfigServersFormGroup.disable({emitEvent: false});
if (serverConfigs?.length === this.serverConfigsFromArray.length) {
this.serverConfigsFromArray.patchValue(serverConfigs, {emitEvent: false});
} else {
this.bootstrapConfigServersFormGroup.enable({emitEvent: false});
const serverConfigsControls: Array<AbstractControl> = [];
if (serverConfigs) {
serverConfigs.forEach((serverConfig) => {
serverConfigsControls.push(this.fb.control(serverConfig));
});
}
this.bootstrapConfigServersFormGroup.setControl('serverConfigs', this.fb.array(serverConfigsControls), {emitEvent: false});
if (this.disabled) {
this.bootstrapConfigServersFormGroup.disable({emitEvent: false});
} else {
this.bootstrapConfigServersFormGroup.enable({emitEvent: false});
}
}
this.valueChangeSubscription = this.bootstrapConfigServersFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
trackByParams(index: number): number {
@ -147,7 +152,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
true
).subscribe((result) => {
if (result) {
this.serverConfigsFromArray().removeAt(index);
this.serverConfigsFromArray.removeAt(index);
}
});
}
@ -169,7 +174,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
addServerConfigObs.subscribe((serverConfig) => {
if (serverConfig) {
serverConfig.securityMode = Lwm2mSecurityType.NO_SEC;
this.serverConfigsFromArray().push(this.fb.control(serverConfig));
this.serverConfigsFromArray.push(this.fb.control(serverConfig));
this.updateModel();
} else {
this.isTransportWasRunWithBootstrap = false;
@ -196,7 +201,7 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
}
private isBootstrapAdded(): boolean {
const serverConfigsArray = this.serverConfigsFromArray().getRawValue();
const serverConfigsArray = this.serverConfigsFromArray.getRawValue();
for (let i = 0; i < serverConfigsArray.length; i++) {
if (serverConfigsArray[i].bootstrapServerIs) {
return true;
@ -207,15 +212,15 @@ export class Lwm2mBootstrapConfigServersComponent implements OnInit, ControlValu
private removeBootstrapServerConfig(): void {
if (this.bootstrapConfigServersFormGroup) {
const bootstrapServerIndex = this.serverConfigsFromArray().getRawValue().findIndex(server => server.bootstrapServerIs === true);
const bootstrapServerIndex = this.serverConfigsFromArray.getRawValue().findIndex(server => server.bootstrapServerIs === true);
if (bootstrapServerIndex !== -1) {
this.serverConfigsFromArray().removeAt(bootstrapServerIndex);
this.serverConfigsFromArray.removeAt(bootstrapServerIndex);
}
}
}
private updateModel() {
const serverConfigs: Array<ServerSecurityConfig> = this.serverConfigsFromArray().value;
const serverConfigs: Array<ServerSecurityConfig> = this.serverConfigsFromArray.value;
this.propagateChange(serverConfigs);
}
}

View File

@ -80,6 +80,8 @@ export class Lwm2mObserveAttrTelemetryInstancesComponent implements ControlValue
this.instancesFormGroup = this.fb.group({
instances: this.fb.array([])
});
this.valueChange$ = this.instancesFormGroup.valueChanges.subscribe(value => this.updateModel(value.instances));
}
ngOnDestroy() {
@ -122,22 +124,16 @@ export class Lwm2mObserveAttrTelemetryInstancesComponent implements ControlValue
if (instances.length === this.instancesFormArray.length) {
this.instancesFormArray.patchValue(instances, {emitEvent: false});
} else {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const instancesControl: Array<AbstractControl> = [];
if (instances) {
instances.forEach((instance) => {
instancesControl.push(this.createInstanceFormGroup(instance));
});
}
this.instancesFormGroup.setControl('instances', this.fb.array(instancesControl));
this.instancesFormGroup.setControl('instances', this.fb.array(instancesControl), {emitEvent: false});
if (this.disabled) {
this.instancesFormGroup.disable({emitEvent: false});
}
this.valueChange$ = this.instancesFormGroup.valueChanges.subscribe(value => {
this.updateModel(value.instances);
});
}
}

View File

@ -29,7 +29,7 @@ import {
} from '@angular/forms';
import { ResourceLwM2M } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { combineLatest, Subject, Subscription } from 'rxjs';
import { combineLatest, Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
@Component({
@ -71,19 +71,19 @@ export class Lwm2mObserveAttrTelemetryResourcesComponent implements ControlValue
}
private destroy$ = new Subject<void>();
private valueChange$: Subscription = null;
private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder) {
this.resourcesFormGroup = this.fb.group({
resources: this.fb.array([])
});
this.resourcesFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel(this.resourcesFormGroup.getRawValue().resources));
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
@ -131,24 +131,18 @@ export class Lwm2mObserveAttrTelemetryResourcesComponent implements ControlValue
private updatedResources(resources: ResourceLwM2M[]): void {
if (resources.length === this.resourcesFormArray.length) {
this.resourcesFormArray.patchValue(resources, {onlySelf: true});
this.resourcesFormArray.patchValue(resources, {onlySelf: true, emitEvent: false});
} else {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const resourcesControl: Array<AbstractControl> = [];
if (resources) {
resources.forEach((resource) => {
resourcesControl.push(this.createdResourceFormGroup(resource));
});
}
this.resourcesFormGroup.setControl('resources', this.fb.array(resourcesControl));
this.resourcesFormGroup.setControl('resources', this.fb.array(resourcesControl), {emitEvent: false});
if (this.disabled) {
this.resourcesFormGroup.disable({emitEvent: false});
}
this.valueChange$ = this.resourcesFormGroup.valueChanges.subscribe(() => {
this.updateModel(this.resourcesFormGroup.getRawValue().resources);
});
}
}

View File

@ -86,6 +86,8 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor,
this.modelsFormGroup = this.fb.group({
models: this.fb.array([])
});
this.valueChange$ = this.modelsFormGroup.valueChanges.subscribe(value => this.updateModel(value.models));
}
ngOnDestroy() {
@ -130,20 +132,14 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor,
if (models.length === this.modelsFormArray.length) {
this.modelsFormArray.patchValue(models, {emitEvent: false});
} else {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const modelControls: Array<AbstractControl> = [];
models.forEach(model => {
modelControls.push(this.createModelFormGroup(model));
});
this.modelsFormGroup.setControl('models', this.fb.array(modelControls));
this.modelsFormGroup.setControl('models', this.fb.array(modelControls), {emitEvent: false});
if (this.disabled) {
this.modelsFormGroup.disable({emitEvent: false});
}
this.valueChange$ = this.modelsFormGroup.valueChanges.subscribe(value => {
this.updateModel(value.models);
});
}
}

View File

@ -21,24 +21,15 @@
</mat-checkbox>
<div *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value"
class="tb-hint" innerHTML="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-hint' | translate }}"></div>
<mat-form-field floatLabel="always" class="mat-block" style="padding-top: 8px;"
*ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value">
<mat-label translate>device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names</mat-label>
<mat-chip-list #attrMetricNamesChipList formControlName="sparkplugAttributesMetricNames">
<mat-chip
*ngFor="let name of mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value;"
(removed)="removeAttributeMetricName(name)">
{{name}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names' | translate}}"
[matChipInputFor]="attrMetricNamesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAttributeMetricName($event)">
</mat-chip-list>
<mat-hint innerHTML="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint' | translate }}"></mat-hint>
</mat-form-field>
<tb-string-items-list *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value"
editable
label="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names' | translate }}"
placeholder="{{'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names' | translate}}"
hint="{{ 'device-profile.mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint' | translate }}"
floatLabel="always"
subscriptSizing="dynamic"
formControlName="sparkplugAttributesMetricNames">
</tb-string-items-list>
</form>
<form [formGroup]="mqttDeviceProfileTransportConfigurationFormGroup" style="padding-bottom: 16px;" *ngIf="!mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplug').value">
<fieldset class="fields-group">

View File

@ -41,8 +41,6 @@ import {
import { isDefinedAndNotNull } from '@core/utils';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
@Component({
selector: 'tb-mqtt-device-profile-transport-configuration',
@ -79,8 +77,6 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control
private propagateChange = (v: any) => { };
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
constructor(private store: Store<AppState>,
private fb: UntypedFormBuilder) {
}
@ -175,34 +171,6 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control
}
}
removeAttributeMetricName(name: string): void {
const names: string[] = this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value;
const index = names.indexOf(name);
if (index >= 0) {
names.splice(index, 1);
this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').setValue(names);
}
}
addAttributeMetricName(event: MatChipInputEvent): void {
const input = event.input;
let value = event.value;
if ((value || '').trim()) {
value = value.trim();
let names: string[] = this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').value;
if (!names || names.indexOf(value) === -1) {
if (!names) {
names = [];
}
names.push(value);
this.mqttDeviceProfileTransportConfigurationFormGroup.get('sparkplugAttributesMetricNames').setValue(names, {emitEvent: true});
}
}
if (input) {
input.value = '';
}
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) {

View File

@ -16,7 +16,7 @@
-->
<div fxLayout="column">
<div *ngFor="let deviceProfileCommunication of communicationConfigFormArray().controls; let $index = index;
<div *ngFor="let deviceProfileCommunication of communicationConfigFormArray.controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication">
<div class="communication-config" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
@ -58,7 +58,7 @@
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!communicationConfigFormArray().controls.length && !disabled">
<div *ngIf="!communicationConfigFormArray.controls.length && !disabled">
<span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-communication-config</span>
</div>
<div *ngIf="!disabled && isAddEnabled">

View File

@ -27,7 +27,7 @@ import {
Validators
} from '@angular/forms';
import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models';
import { Subject, Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { isUndefinedOrNull } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
@ -58,7 +58,6 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
disabled: boolean;
private usedSpecType: SnmpSpecType[] = [];
private valueChange$: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
@ -68,17 +67,17 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
this.deviceProfileCommunicationConfig = this.fb.group({
communicationConfig: this.fb.array([])
});
this.deviceProfileCommunicationConfig.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
communicationConfigFormArray(): UntypedFormArray {
get communicationConfigFormArray(): UntypedFormArray {
return this.deviceProfileCommunicationConfig.get('communicationConfig') as UntypedFormArray;
}
@ -99,27 +98,27 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
}
writeValue(communicationConfig: SnmpCommunicationConfig[]) {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const communicationConfigControl: Array<AbstractControl> = [];
if (communicationConfig) {
communicationConfig.forEach((config) => {
communicationConfigControl.push(this.createdFormGroup(config));
});
}
this.deviceProfileCommunicationConfig.setControl('communicationConfig', this.fb.array(communicationConfigControl));
if (!communicationConfig || !communicationConfig.length) {
this.addCommunicationConfig();
}
if (this.disabled) {
this.deviceProfileCommunicationConfig.disable({emitEvent: false});
if (communicationConfig?.length === this.communicationConfigFormArray.length) {
this.communicationConfigFormArray.patchValue(communicationConfig, {emitEvent: false});
} else {
this.deviceProfileCommunicationConfig.enable({emitEvent: false});
const communicationConfigControl: Array<AbstractControl> = [];
if (communicationConfig) {
communicationConfig.forEach((config) => {
communicationConfigControl.push(this.createdFormGroup(config));
});
}
this.deviceProfileCommunicationConfig.setControl(
'communicationConfig', this.fb.array(communicationConfigControl), {emitEvent: false}
);
if (!communicationConfig || !communicationConfig.length) {
this.addCommunicationConfig();
}
if (this.disabled) {
this.deviceProfileCommunicationConfig.disable({emitEvent: false});
} else {
this.deviceProfileCommunicationConfig.enable({emitEvent: false});
}
}
this.valueChange$ = this.deviceProfileCommunicationConfig.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedSpecType();
if (!this.disabled && !this.deviceProfileCommunicationConfig.valid) {
this.updateModel();
@ -133,16 +132,16 @@ export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, On
}
public removeCommunicationConfig(index: number) {
this.communicationConfigFormArray().removeAt(index);
this.communicationConfigFormArray.removeAt(index);
}
get isAddEnabled(): boolean {
return this.communicationConfigFormArray().length !== Object.keys(SnmpSpecType).length;
return this.communicationConfigFormArray.length !== Object.keys(SnmpSpecType).length;
}
public addCommunicationConfig() {
this.communicationConfigFormArray().push(this.createdFormGroup());
this.communicationConfigFormArray.push(this.createdFormGroup());
this.deviceProfileCommunicationConfig.updateValueAndValidity();
if (!this.deviceProfileCommunicationConfig.valid) {
this.updateModel();

View File

@ -25,7 +25,7 @@
</div>
</div>
<mat-divider></mat-divider>
<div *ngFor="let mappingConfig of mappingsConfigFormArray().controls; let $index = index;
<div *ngFor="let mappingConfig of mappingsConfigFormArray.controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list">
<div fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
@ -64,7 +64,7 @@
</button>
</div>
</div>
<div *ngIf="!mappingsConfigFormArray().controls.length && !disabled">
<div *ngIf="!mappingsConfigFormArray.controls.length && !disabled">
<span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-mapping-config</span>
</div>
<div *ngIf="!disabled">

View File

@ -18,19 +18,20 @@ import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { SnmpMapping } from '@shared/models/device.models';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { DataType, DataTypeTranslationMap } from '@shared/models/constants';
import { isUndefinedOrNull } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-snmp-device-profile-mapping',
@ -60,7 +61,7 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
private readonly oidPattern: RegExp = /^\.?([0-2])((\.0)|(\.[1-9][0-9]*))*$/;
private valueChange$: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder) { }
@ -69,12 +70,14 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
this.mappingsConfigForm = this.fb.group({
mappings: this.fb.array([])
});
this.mappingsConfigForm.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any) {
@ -100,38 +103,36 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
}
writeValue(mappings: SnmpMapping[]) {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
const mappingsControl: Array<AbstractControl> = [];
if (mappings) {
mappings.forEach((config) => {
mappingsControl.push(this.createdFormGroup(config));
});
}
this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl));
if (!mappings || !mappings.length) {
this.addMappingConfig();
}
if (this.disabled) {
this.mappingsConfigForm.disable({emitEvent: false});
if (mappings?.length === this.mappingsConfigFormArray.length) {
this.mappingsConfigFormArray.patchValue(mappings, {emitEvent: false});
} else {
this.mappingsConfigForm.enable({emitEvent: false});
const mappingsControl: Array<AbstractControl> = [];
if (mappings) {
mappings.forEach((config) => {
mappingsControl.push(this.createdFormGroup(config));
});
}
this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl), {emitEvent: false});
if (!mappings || !mappings.length) {
this.addMappingConfig();
}
if (this.disabled) {
this.mappingsConfigForm.disable({emitEvent: false});
} else {
this.mappingsConfigForm.enable({emitEvent: false});
}
}
this.valueChange$ = this.mappingsConfigForm.valueChanges.subscribe(() => {
this.updateModel();
});
if (!this.disabled && !this.mappingsConfigForm.valid) {
this.updateModel();
}
}
mappingsConfigFormArray(): UntypedFormArray {
get mappingsConfigFormArray(): UntypedFormArray {
return this.mappingsConfigForm.get('mappings') as UntypedFormArray;
}
public addMappingConfig() {
this.mappingsConfigFormArray().push(this.createdFormGroup());
this.mappingsConfigFormArray.push(this.createdFormGroup());
this.mappingsConfigForm.updateValueAndValidity();
if (!this.mappingsConfigForm.valid) {
this.updateModel();
@ -139,7 +140,7 @@ export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, Con
}
public removeMappingConfig(index: number) {
this.mappingsConfigFormArray().removeAt(index);
this.mappingsConfigFormArray.removeAt(index);
}
private createdFormGroup(value?: SnmpMapping): UntypedFormGroup {

View File

@ -30,10 +30,11 @@ import {
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { QueueInfo } from '@shared/models/queue.models';
import { UtilsService } from '@core/services/utils.service';
import { guid } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-tenant-profile-queues',
@ -70,8 +71,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
@Input()
disabled: boolean;
private valueChangeSubscription$: Subscription = null;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@ -83,12 +83,6 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
this.propagateChange = fn;
}
ngOnDestroy() {
if (this.valueChangeSubscription$) {
this.valueChangeSubscription$.unsubscribe();
}
}
registerOnTouched(fn: any): void {
}
@ -96,6 +90,15 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
this.tenantProfileQueuesFormGroup = this.fb.group({
queues: this.fb.array([])
});
this.tenantProfileQueuesFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get queuesFormArray(): UntypedFormArray {
@ -112,30 +115,28 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
}
writeValue(queues: Array<QueueInfo> | null): void {
if (this.valueChangeSubscription$) {
this.valueChangeSubscription$.unsubscribe();
}
const queuesControls: Array<AbstractControl> = [];
if (queues) {
queues.forEach((queue, index) => {
if (!queue.id) {
if (!this.idMap[index]) {
this.idMap.push(guid());
}
queue.id = this.idMap[index];
}
queuesControls.push(this.fb.control(queue, [Validators.required]));
});
}
this.tenantProfileQueuesFormGroup.setControl('queues', this.fb.array(queuesControls));
if (this.disabled) {
this.tenantProfileQueuesFormGroup.disable({emitEvent: false});
if (queues.length === this.queuesFormArray.length) {
this.queuesFormArray.patchValue(queues, {emitEvent: false});
} else {
this.tenantProfileQueuesFormGroup.enable({emitEvent: false});
const queuesControls: Array<AbstractControl> = [];
if (queues) {
queues.forEach((queue, index) => {
if (!queue.id) {
if (!this.idMap[index]) {
this.idMap.push(guid());
}
queue.id = this.idMap[index];
}
queuesControls.push(this.fb.control(queue, [Validators.required]));
});
}
this.tenantProfileQueuesFormGroup.setControl('queues', this.fb.array(queuesControls), {emitEvent: false});
if (this.disabled) {
this.tenantProfileQueuesFormGroup.disable({emitEvent: false});
} else {
this.tenantProfileQueuesFormGroup.enable({emitEvent: false});
}
}
this.valueChangeSubscription$ = this.tenantProfileQueuesFormGroup.valueChanges.subscribe(() =>
this.updateModel()
);
}
public trackByQueue(index: number, queueControl: AbstractControl) {

View File

@ -45,7 +45,7 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.transport-device-msg'],
[RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-device-telemetry-msg'],
[RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-device-telemetry-data-points'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.transport-tenant-msg-rate-limit'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rest-requests-for-tenant'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.customer-rest-limits'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.ws-limit-updates-per-session'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.cassandra-tenant-limits-configuration'],
@ -63,7 +63,7 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.edit-transport-device-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.edit-transport-device-telemetry-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.edit-transport-device-telemetry-data-points-title'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-transport-tenant-msg-rate-limit-title'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-tenant-rest-limits-title'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-customer-rest-limits-title'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.rate-limits.edit-ws-limit-updates-per-session-title'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-cassandra-tenant-limits-configuration-title'],

View File

@ -17,17 +17,17 @@
-->
<div class="tb-relation-filters" [formGroup]="relationFiltersFormGroup">
<div class="container">
<div class="header" [fxShow]="relationFiltersFormArray().length">
<div class="header" [fxShow]="relationFiltersFormArray.length">
<div fxLayout="row" fxLayoutAlign="start center">
<span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span>
<span class="cell" fxFlex translate>entity.entity-types</span>
<span class="cell" style="width: 40px; min-width: 40px;">&nbsp;</span>
</div>
</div>
<div class="body" [fxShow]="relationFiltersFormArray().length">
<div class="body" [fxShow]="relationFiltersFormArray.length">
<div class="row" fxFlex fxLayout="row"
fxLayoutAlign="start center" formArrayName="relationFilters"
*ngFor="let relationFilterControl of relationFiltersFormArray().controls; let $index = index">
*ngFor="let relationFilterControl of relationFiltersFormArray.controls; let $index = index">
<div class="mat-elevation-z1" fxFlex fxLayout="row" fxLayoutAlign="start center" style="padding: 8px 0;">
<tb-relation-type-autocomplete
class="cell" style="width: 200px; min-width: 200px;"
@ -51,7 +51,7 @@
</div>
</div>
</div>
<div class="any-filter" [fxShow]="!relationFiltersFormArray().length">
<div class="any-filter" [fxShow]="!relationFiltersFormArray.length">
<span fxLayoutAlign="center center"
class="tb-prompt" translate>relation.any-relation</span>
</div>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -28,7 +28,8 @@ import { RelationEntityTypeFilter } from '@shared/models/relation.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-relation-filters',
@ -42,7 +43,7 @@ import { Subscription } from 'rxjs';
}
]
})
export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit {
export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input() disabled: boolean;
@ -50,22 +51,32 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
relationFiltersFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
ngOnInit(): void {
this.relationFiltersFormGroup = this.fb.group({});
this.relationFiltersFormGroup.addControl('relationFilters',
this.fb.array([]));
this.relationFiltersFormGroup = this.fb.group({
relationFilters: this.fb.array([])
});
this.relationFiltersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.updateModel();
});
}
relationFiltersFormArray(): UntypedFormArray {
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get relationFiltersFormArray(): UntypedFormArray {
return this.relationFiltersFormGroup.get('relationFilters') as UntypedFormArray;
}
@ -81,19 +92,17 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
}
writeValue(filters: Array<RelationEntityTypeFilter>): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
if (filters?.length === this.relationFiltersFormArray.length) {
this.relationFiltersFormArray.patchValue(filters, {emitEvent: false});
} else {
const relationFiltersControls: Array<AbstractControl> = [];
if (filters && filters.length) {
filters.forEach((filter) => {
relationFiltersControls.push(this.createRelationFilterFormGroup(filter));
});
}
this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls), {emitEvent: false});
}
const relationFiltersControls: Array<AbstractControl> = [];
if (filters && filters.length) {
filters.forEach((filter) => {
relationFiltersControls.push(this.createRelationFilterFormGroup(filter));
});
}
this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls));
this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
public removeFilter(index: number) {
@ -101,12 +110,11 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
}
public addFilter() {
const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as UntypedFormArray;
const filter: RelationEntityTypeFilter = {
relationType: null,
entityTypes: []
};
relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
this.relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
}
private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl {

View File

@ -15,8 +15,10 @@
limitations under the License.
-->
<div>
<mat-card appearance="outlined" class="repository-settings" [ngClass]="{'settings-card': !detailsMode}" [ngStyle]="popoverComponent ? {boxShadow: 'none', width: '800px'} : {}">
<div style="height: min-content; max-height: 80vh;">
<mat-card appearance="outlined" class="repository-settings"
[ngClass]="{'settings-card': !detailsMode}"
[ngStyle]="popoverComponent ? {boxShadow: 'none', maxWidth: '100%', minWidth: '100%', width: '800px'} : {}">
<mat-card-header>
<mat-card-title>
<span class="mat-headline-5" translate>admin.repository-settings</span>
@ -41,10 +43,10 @@
<mat-label translate>admin.default-branch</mat-label>
<input matInput formControlName="defaultBranch">
</mat-form-field>
<mat-checkbox formControlName="readOnly">
{{ 'admin.repository-read-only' | translate }}
</mat-checkbox>
<div>
<div fxLayout="column">
<mat-checkbox formControlName="readOnly">
{{ 'admin.repository-read-only' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showMergeCommits">
{{ 'admin.show-merge-commits' | translate }}
</mat-checkbox>
@ -74,6 +76,7 @@
<input matInput formControlName="password" type="password"
placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/>
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-hint [innerHTML] = "'admin.auth-method-username-password-hint' | translate"></mat-hint>
</mat-form-field>
</section>
<section [fxShow]="repositorySettingsForm.get('authMethod').value === repositoryAuthMethod.PRIVATE_KEY" fxLayout="column">

View File

@ -17,9 +17,6 @@
.mat-mdc-card.repository-settings {
margin: 8px;
}
.mat-mdc-checkbox {
padding-bottom: 16px;
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;

View File

@ -101,7 +101,7 @@
&.comparison {
.mat-expansion-panel-header {
height: 100%;
height: fit-content;
}
}

View File

@ -804,11 +804,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (descriptors.length) {
let entityId;
let entityName;
let entityLabel;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.originatorName;
entityLabel = alarm.originatorLabel;
}
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm});
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm}, entityLabel);
}
}
@ -827,11 +829,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
let entityId;
let entityName;
let entityLabel;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.originatorName;
entityLabel = alarm.originatorLabel;
}
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm});
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm}, entityLabel);
}
}

View File

@ -405,7 +405,7 @@ export class TbCanvasDigitalGauge {
(this.gauge.options as CanvasDigitalGaugeOptions).labelTimestamp =
filter.transform(timestamp, this.localSettings.timestampFormat);
}
const value = tvPair[1];
const value = parseFloat(tvPair[1]);
if (value !== this.gauge.value) {
if (!this.gauge.options.animation) {
this.gauge._value = value;

View File

@ -17,7 +17,7 @@
-->
<div fxLayout="column" class="mat-content mat-padding">
<label class="tb-title" translate>entity.columns-to-display</label>
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="column.display" *ngFor="let column of (columns | selectableColumns)"
<mat-checkbox [(ngModel)]="column.display" *ngFor="let column of (columns | selectableColumns)"
(ngModelChange)="update()">
{{ column.title }}
</mat-checkbox>

View File

@ -51,7 +51,7 @@
</mat-option>
</mat-select>
</mat-form-field>
<section fxLayout="column" fxLayoutGap="8px">
<section fxLayout="column" fxLayoutGap.gt-xs="8px">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="displayEntityName">
{{ 'widgets.table.display-entity-name' | translate }}

View File

@ -47,7 +47,7 @@
</mat-form-field>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.label-widget.label-position</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>widgets.label-widget.x-pos</mat-label>
<input matInput type="number" min="0" max="100" formControlName="x">

View File

@ -24,7 +24,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.border-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.border-width</mat-label>
<input matInput type="number" min="0" formControlName="borderWidth">

View File

@ -41,7 +41,7 @@
</fieldset>
<fieldset class="fields-group" formGroupName="stroke">
<legend class="group-title" translate>widgets.chart.stroke-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.width-pixels</mat-label>
<input matInput type="number" min="0" formControlName="width">

View File

@ -16,7 +16,7 @@
-->
<section [formGroup]="widgetFontFormGroup" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.widget-font.font-family</mat-label>
<input matInput formControlName="family">
@ -26,7 +26,7 @@
<input matInput type="number" min="1" step="1" formControlName="size">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.widget-font.font-style</mat-label>
<mat-select formControlName="style">
@ -68,7 +68,7 @@
</mat-select>
</mat-form-field>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input
fxFlex
formControlName="color"

View File

@ -29,7 +29,7 @@
<mat-label translate>widgets.rpc.initial-value</mat-label>
<input matInput type="number" formControlName="initialValue">
</mat-form-field>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.min-value</mat-label>
<input required matInput type="number" formControlName="minValue">

View File

@ -33,7 +33,7 @@
</mat-checkbox>
</section>
</section>
<section fxLayout="column" fxLayoutGap="8px">
<section fxLayout="column" fxLayoutGap.gt-xs="8px">
<mat-slide-toggle fxFlex formControlName="displayDetails">
{{ 'widgets.persistent-table.display-request-details' | translate }}
</mat-slide-toggle>

View File

@ -24,7 +24,7 @@
<mat-slide-toggle formControlName="isPrimary" class="slide-block">
{{ 'widgets.rpc.button-primary' | translate }}
</mat-slide-toggle>
<section fxLayout="row wrap" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="textColor"
icon="format_color_fill"

View File

@ -22,7 +22,7 @@
<mat-label translate>widgets.rpc.slide-toggle-label</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.rpc.label-position</mat-label>
<mat-select formControlName="labelPosition">

View File

@ -42,7 +42,7 @@
icon="format_color_fill"
label="{{ 'widgets.gauge.major-ticks-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.minor-ticks-count</mat-label>
<input matInput type="number" min="0" formControlName="minorTicks">
@ -71,7 +71,7 @@
<mat-slide-toggle formControlName="showBorder" class="slide-block">
{{ 'widgets.gauge.show-plate-border' | translate }}
</mat-slide-toggle>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<tb-color-input fxFlex
formControlName="colorBorder"
icon="format_color_fill"
@ -89,7 +89,7 @@
<mat-label translate>widgets.gauge.needle-circle-size</mat-label>
<input matInput type="number" min="0" formControlName="needleCircleSize">
</mat-form-field>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<tb-color-input fxFlex
formControlName="colorNeedle"
icon="format_color_fill"

View File

@ -31,7 +31,7 @@
</tb-color-input>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.ticks-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.min-value</mat-label>
<input matInput type="number" formControlName="minValue">
@ -41,7 +41,7 @@
<input matInput type="number" formControlName="maxValue">
</mat-form-field>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="0px" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.major-ticks-count</mat-label>
<input matInput type="number" min="0" formControlName="majorTicksCount">
@ -52,7 +52,7 @@
label="{{ 'widgets.gauge.major-ticks-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="0px" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.minor-ticks-count</mat-label>
<input matInput type="number" min="0" formControlName="minorTicks">
@ -127,7 +127,7 @@
<legend class="group-title" translate>widgets.gauge.value-font</legend>
<tb-widget-font formControlName="valueFont" [hasShadowColor]="true"></tb-widget-font>
</fieldset>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorValueBoxRect"
icon="format_color_fill"
@ -139,7 +139,7 @@
label="{{ 'widgets.gauge.value-box-rect-stroke-color-end' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorValueBoxBackground"
icon="format_color_fill"
@ -168,7 +168,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.needle-settings</legend>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorNeedle"
icon="format_color_fill"
@ -180,7 +180,7 @@
label="{{ 'widgets.gauge.needle-color-end' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorNeedleShadowUp"
icon="format_color_fill"
@ -294,7 +294,7 @@
<ng-template #radialGauge let-settingsForm="settingsForm">
<fieldset class="fields-group" [formGroup]="settingsForm">
<legend class="group-title" translate>widgets.gauge.radial-gauge-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.start-ticks-angle</mat-label>
<input matInput type="number" min="0" max="360" formControlName="startAngle">
@ -314,7 +314,7 @@
<ng-template #linearGauge let-settingsForm="settingsForm">
<fieldset class="fields-group" [formGroup]="settingsForm">
<legend class="group-title" translate>widgets.gauge.linear-gauge-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.bar-stroke-width</mat-label>
<input matInput type="number" min="0" formControlName="barStrokeWidth">
@ -325,7 +325,7 @@
label="{{ 'widgets.gauge.bar-stroke-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorBar"
icon="format_color_fill"
@ -337,7 +337,7 @@
label="{{ 'widgets.gauge.bar-background-color-end' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<section fxLayout="row wrap" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-color-input fxFlex
formControlName="colorBarProgress"
icon="format_color_fill"

View File

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="digitalGaugeWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.common-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gauge.min-value</mat-label>
<input matInput type="number" formControlName="minValue">
@ -182,7 +182,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.gauge.unit-title-and-timestamp-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="showUnitTitle">
{{ 'widgets.gauge.show-unit-title' | translate }}
</mat-slide-toggle>
@ -191,7 +191,7 @@
<input matInput formControlName="unitTitle">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="showTimestamp">
{{ 'widgets.gauge.show-timestamp' | translate }}
</mat-slide-toggle>

View File

@ -40,7 +40,7 @@
<div fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider>
<section class="tb-widget-settings" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex>
<mat-label translate>widgets.gauge.highlight-from</mat-label>
<input matInput type="number" formControlName="from">

View File

@ -45,7 +45,7 @@
<div fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider>
<section class="tb-widget-settings" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gpio.pin</mat-label>
<input required matInput type="number" min="1" step="1" formControlName="pin">
@ -55,7 +55,7 @@
<input required matInput formControlName="label">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.gpio.row</mat-label>
<input required matInput type="number" min="0" step="1" formControlName="row">

View File

@ -28,7 +28,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.checkbox-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.true-label</mat-label>
<input matInput formControlName="trueValue">

View File

@ -21,7 +21,7 @@
</tb-update-attribute-general-settings>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.double-field-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.min-value</mat-label>
<input matInput type="number" step="any" formControlName="minValue">

View File

@ -21,7 +21,7 @@
</tb-update-attribute-general-settings>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.integer-field-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.min-value</mat-label>
<input matInput type="number" step="1" formControlName="minValue">

View File

@ -37,7 +37,7 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.attribute-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.widget-mode</mat-label>
<mat-select formControlName="widgetMode">

View File

@ -25,7 +25,7 @@
<mat-slide-toggle formControlName="showResultMessage" class="slide-block">
{{ 'widgets.input-widgets.show-result-message' | translate }}
</mat-slide-toggle>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.latitude-key-name</mat-label>
<input matInput formControlName="latKeyName">
@ -47,7 +47,7 @@
<mat-slide-toggle formControlName="showLabel" class="slide-block">
{{ 'widgets.input-widgets.show-label' | translate }}
</mat-slide-toggle>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.latitude-label</mat-label>
<input matInput formControlName="latLabel">

View File

@ -45,7 +45,7 @@
<mat-checkbox formControlName="updateAllValues" style="margin-bottom: 8px;">
{{ 'widgets.input-widgets.update-all-values' | translate }}
</mat-checkbox>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.save-button-label</mat-label>
<input matInput formControlName="saveButtonLabel">

View File

@ -21,7 +21,7 @@
</tb-update-attribute-general-settings>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.text-field-settings</legend>
<section fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.input-widgets.min-length</mat-label>
<input matInput type="number" step="1" min="0" formControlName="minLength">

View File

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="commonMapSettingsFormGroup">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.maps.common-map-settings</legend>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<tb-datasources-key-autocomplete fxFlex
[fxShow]="provider === mapProvider.image"
required
@ -55,7 +55,7 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section [fxShow]="provider !== mapProvider.image" fxLayout="row" fxLayoutGap="8px">
<section [fxShow]="provider !== mapProvider.image" fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.default-map-zoom-level</mat-label>
<input matInput type="number" min="0" max="20" step="1" formControlName="defaultZoomLevel">
@ -65,7 +65,7 @@
<input matInput formControlName="defaultCenterPosition">
</mat-form-field>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" style="margin-bottom: 15px;">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" style="margin-bottom: 15px;">
<section fxFlex fxLayout="column">
<mat-slide-toggle formControlName="disableScrollZooming" class="slide-block">
{{ 'widgets.maps.disable-scroll-zooming' | translate }}

View File

@ -34,7 +34,7 @@
<mat-slide-toggle formControlName="zoomOnClick" class="slide-block">
{{ 'widgets.maps.zoom-on-cluster-click' | translate }}
</mat-slide-toggle>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.max-cluster-zoom</mat-label>
<input matInput type="number" min="0" max="18" step="1" formControlName="maxZoom">

View File

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="markersSettingsFormGroup">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.maps.markers-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.marker-offset-x</mat-label>
<input matInput type="number" formControlName="markerOffsetX">
@ -113,7 +113,7 @@
functionTitle="{{ 'widgets.maps.tooltip-function' | translate }}"
helpId="widget/lib/map/tooltip_fn">
</tb-js-func>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.tooltip-offset-x</mat-label>
<input matInput type="number" formControlName="tooltipOffsetX">

View File

@ -18,7 +18,7 @@
<section class="tb-widget-settings" [formGroup]="routeMapSettingsFormGroup">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.maps.route-map-settings</legend>
<section fxLayout="row" fxLayoutGap="8px">
<section fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.maps.stroke-weight</mat-label>
<input matInput type="number" min="0" formControlName="strokeWeight">

View File

@ -417,9 +417,6 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
'.mat-mdc-table .mat-mdc-header-cell {\n' +
'color: ' + mdDarkSecondary + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-header-cell .mat-sort-header-arrow {\n' +
'color: ' + mdDarkDisabled + ';\n' +
'}\n' +
'.mat-mdc-table .mat-mdc-cell, .mat-mdc-table .mat-mdc-header-cell {\n' +
'border-bottom-color: ' + mdDarkDivider + ';\n' +
'}\n' +

View File

@ -19,8 +19,8 @@
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
<div *ngIf="displayTimewindowConfig()" fxFlex="100"
fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs>
fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxFlex.gt-xs>
<mat-checkbox formControlName="useDashboardTimewindow">
{{ 'widget-config.use-dashboard-timewindow' | translate }}
</mat-checkbox>
@ -29,7 +29,7 @@
</mat-checkbox>
</div>
<section fxLayout="row" fxLayoutAlign="start center" fxLayout.lt-lg="column" fxLayoutAlign.lt-lg="center start"
fxFlex.gt-xs style="margin-bottom: 16px;">
fxFlex.gt-xs>
<span [ngClass]="{'tb-disabled-label': dataSettings.get('useDashboardTimewindow').value}" translate
style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow asButton="true"
@ -43,7 +43,7 @@
</div>
<div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center"
fxLayoutGap="8px">
fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList" multiple
@ -63,8 +63,7 @@
</mat-select>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center"
fxLayoutGap="8px">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-grid #alarmTypeChipList formControlName="alarmTypeList">
@ -355,7 +354,7 @@
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ 'widget-config.display-title' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>widget-config.title</mat-label>
<input matInput formControlName="title">
@ -365,12 +364,12 @@
<input matInput formControlName="titleTooltip">
</mat-form-field>
</div>
<fieldset class="fields-group" fxLayoutGap="8px" style="margin: 0">
<fieldset class="fields-group" fxLayoutGap.gt-xs="8px" style="margin: 0">
<legend class="group-title" translate>widget-config.title-icon</legend>
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row wrap" fxLayoutGap="8px">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<tb-material-icon-select fxFlex
formControlName="titleIcon">
</tb-material-icon-select>
@ -404,8 +403,8 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.widget-style</legend>
<div fxLayout="column" fxLayout.gt-md="row wrap" fxFlex fxLayoutGap="8px" class="tb-widget-style">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" fxFlex>
<div fxLayout="column" fxLayout.gt-md="row wrap" fxFlex fxLayoutGap.gt-xs="8px" class="tb-widget-style">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<tb-color-input fxFlex
label="{{'widget-config.background-color' | translate}}"
icon="format_color_fill"
@ -419,7 +418,7 @@
formControlName="color">
</tb-color-input>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" fxFlex>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.padding</mat-label>
<input matInput formControlName="padding">
@ -477,8 +476,8 @@
<fieldset [formGroup]="layoutSettings" class="fields-group fields-group-slider">
<legend class="group-title" translate>widget-config.mobile-mode-settings</legend>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title fxLayout.xs="column" fxLayoutAlign.xs="center start" fxLayout="row" fxLayoutGap="8px" fxFlex="70">
<mat-expansion-panel-header style="height: fit-content;">
<mat-panel-title fxLayout.xs="column" fxLayoutAlign.xs="center start" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex="70">
<mat-slide-toggle formControlName="mobileHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
@ -491,14 +490,14 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" fxFlex>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.order</mat-label>
<input matInput formControlName="mobileOrder" type="number" step="1">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.height</mat-label>
<input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1">
<input matInput formControlName="mobileHeight" type="number" min="1" step="1">
</mat-form-field>
</div>
</ng-template>

View File

@ -255,7 +255,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
});
this.layoutSettings = this.fb.group({
mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]],
mobileHeight: [null, [Validators.min(1), Validators.max(10), Validators.pattern(/^\d*$/)]],
mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]],
mobileHide: [false],
desktopHide: [false]
});

View File

@ -15,172 +15,172 @@
limitations under the License.
-->
<div>
<mat-toolbar color="primary">
<h2 translate>device.add-device-text</h2>
<span fxFlex></span>
<div [tb-help]="'devices'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step [stepControl]="deviceWizardFormGroup">
<form [formGroup]="deviceWizardFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device.wizard.device-details' | translate}}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('required')">
{{ 'device.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('maxlength')">
{{ 'device.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.label</mat-label>
<input matInput formControlName="label">
<mat-error *ngIf="deviceWizardFormGroup.get('label').hasError('maxlength')">
{{ 'device.label-max-length' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="16px">
<mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
<mat-radio-button [value]="0" color="primary">
<span translate>device.wizard.existing-device-profile</span>
</mat-radio-button>
<mat-radio-button [value]="1" color="primary">
<span translate>device.wizard.new-device-profile</span>
</mat-radio-button>
</mat-radio-group>
<div fxLayout="column">
<tb-device-profile-autocomplete
[required]="!createProfile"
formControlName="deviceProfileId"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[editProfileEnabled]="false"
(deviceProfileChanged)="deviceProfileChanged($event)">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}">
<mat-label translate>device-profile.new-device-profile-name</mat-label>
<input matInput formControlName="newDeviceProfileTitle"
[required]="createProfile">
<mat-error *ngIf="deviceWizardFormGroup.get('newDeviceProfileTitle').hasError('required')">
{{ 'device-profile.new-device-profile-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-rule-chain-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-queue-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
</div>
<mat-toolbar color="primary">
<h2 translate>device.add-device-text</h2>
<span fxFlex></span>
<div [tb-help]="'devices'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step [stepControl]="deviceWizardFormGroup">
<form [formGroup]="deviceWizardFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device.wizard.device-details' | translate}}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('required')">
{{ 'device.name-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('maxlength')">
{{ 'device.name-max-length' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.label</mat-label>
<input matInput formControlName="label">
<mat-error *ngIf="deviceWizardFormGroup.get('label').hasError('maxlength')">
{{ 'device.label-max-length' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="16px">
<mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
<mat-radio-button [value]="0" color="primary">
<span translate>device.wizard.existing-device-profile</span>
</mat-radio-button>
<mat-radio-button [value]="1" color="primary">
<span translate>device.wizard.new-device-profile</span>
</mat-radio-button>
</mat-radio-group>
<div fxLayout="column">
<tb-device-profile-autocomplete
[required]="!createProfile"
formControlName="deviceProfileId"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[editProfileEnabled]="false"
(deviceProfileChanged)="deviceProfileChanged($event)">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}">
<mat-label translate>device-profile.new-device-profile-name</mat-label>
<input matInput formControlName="newDeviceProfileTitle"
[required]="createProfile">
<mat-error *ngIf="deviceWizardFormGroup.get('newDeviceProfileTitle').hasError('required')">
{{ 'device-profile.new-device-profile-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<mat-checkbox formControlName="gateway">
{{ 'device.is-gateway' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="deviceWizardFormGroup.get('gateway').value"
formControlName="overwriteActivityTime">
{{ 'device.overwrite-activity-time' | translate }}
</mat-checkbox>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-rule-chain-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
</div>
<mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template><mat-form-field class="mat-block" style="padding-bottom: 14px;">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
<mat-step [stepControl]="credentialsFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
<form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;">
<mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credentials' | translate }}</mat-checkbox>
<tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</form>
</mat-step>
<mat-step [stepControl]="customerFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'customer.customer' | translate }}</ng-template>
<form [formGroup]="customerFormGroup" style="padding-bottom: 16px;">
<tb-entity-autocomplete
formControlName="customerId"
labelText="device.wizard.customer-to-assign-device"
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="row">
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-queue-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
</div>
</div>
<div fxLayout="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<mat-checkbox formControlName="gateway">
{{ 'device.is-gateway' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="deviceWizardFormGroup.get('gateway').value"
formControlName="overwriteActivityTime">
{{ 'device.overwrite-activity-time' | translate }}
</mat-checkbox>
</div>
<mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template><mat-form-field class="mat-block" style="padding-bottom: 14px;">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
<mat-step [stepControl]="credentialsFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
<form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;">
<mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credentials' | translate }}</mat-checkbox>
<tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</form>
</mat-step>
<mat-step [stepControl]="customerFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'customer.customer' | translate }}</ng-template>
<form [formGroup]="customerFormGroup" style="padding-bottom: 16px;">
<tb-entity-autocomplete
formControlName="customerId"
labelText="device.wizard.customer-to-assign-device"
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions style="padding: 0">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center">
<button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
@ -191,8 +191,8 @@
[disabled]="(isLoading$ | async)"
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end">
<mat-divider style="width: 100%"></mat-divider>
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end center">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>

View File

@ -15,6 +15,15 @@
*/
@import "../../../../../scss/constants";
:host {
height: 100%;
display: grid;
.dialog-actions-row {
padding: 8px;
}
}
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) {
@media #{$mat-lt-sm} {
.mat-mdc-dialog-content {
@ -36,31 +45,21 @@
.mat-stepper-horizontal {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
@media #{$mat-lt-sm} {
.mat-step-label {
white-space: normal;
overflow: visible;
.mat-step-text-label {
overflow: visible;
}
}
.mat-horizontal-stepper-wrapper {
flex: 1 1 100%;
}
.mat-horizontal-content-container {
height: 530px;
height: 680px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
scrollbar-gutter: stable;
@media #{$mat-gt-sm} {
min-width: 800px;
}
}
.mat-horizontal-stepper-content[aria-expanded=true] {
height: 100%;
form {
height: 100%;
min-width: 500px;
}
}
}

View File

@ -124,13 +124,13 @@
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
<div style="padding-bottom: 16px;" translate>dashboard.mobile-app-settings</div>
<div translate>dashboard.mobile-app-settings</div>
<tb-image-input fxFlex
label="{{'dashboard.image' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-checkbox fxFlex formControlName="mobileHide" style="padding-bottom: 16px; padding-top: 16px;">
<mat-checkbox fxFlex formControlName="mobileHide">
{{ 'dashboard.mobile-hide' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block">

View File

@ -15,6 +15,7 @@
*/
:host {
button.tb-add-new-widget {
height: auto;
padding-right: 12px;
font-size: 24px;
border-style: dashed;

View File

@ -16,6 +16,7 @@
import {
Component,
ElementRef,
forwardRef,
Inject,
Injector,
@ -45,8 +46,7 @@ import { WINDOW } from '@core/services/window.service';
import { ComponentPortal } from '@angular/cdk/portal';
import {
DASHBOARD_SELECT_PANEL_DATA,
DashboardSelectPanelComponent,
DashboardSelectPanelData
DashboardSelectPanelComponent
} from './dashboard-select-panel.component';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -97,6 +97,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
private overlay: Overlay,
private breakpointObserver: BreakpointObserver,
private viewContainerRef: ViewContainerRef,
private nativeElement: ElementRef,
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window) {
}
@ -131,77 +132,48 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
}
openDashboardSelectPanel() {
if (this.disabled) {
return;
}
const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150;
const panelWidth = 300;
const position = this.overlay.position();
const config = new OverlayConfig({
panelClass: 'tb-dashboard-select-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
});
const el = this.dashboardSelectPanelOrigin.elementRef.nativeElement;
const offset = el.getBoundingClientRect();
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
const bottomY = offset.bottom - scrollTop;
const leftX = offset.left - scrollLeft;
let originX;
let originY;
let overlayX;
let overlayY;
const wHeight = this.document.documentElement.clientHeight;
const wWidth = this.document.documentElement.clientWidth;
if (bottomY + panelHeight > wHeight) {
originY = 'top';
overlayY = 'bottom';
} else {
originY = 'bottom';
overlayY = 'top';
}
if (leftX + panelWidth > wWidth) {
originX = 'end';
overlayX = 'end';
} else {
originX = 'start';
overlayX = 'start';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX,
overlayY
};
config.positionStrategy = position.flexibleConnectedTo(this.dashboardSelectPanelOrigin.elementRef)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
if (!this.disabled) {
const config = new OverlayConfig({
panelClass: 'tb-dashboard-select-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true
});
const injector = this._createDashboardSelectPanelInjector(
overlayRef,
{
dashboards$: this.dashboards$,
dashboardId: this.dashboardId,
onDashboardSelected: (dashboardId) => {
overlayRef.dispose();
this.dashboardId = dashboardId;
this.updateView();
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: DASHBOARD_SELECT_PANEL_DATA,
useValue: {
dashboards$: this.dashboards$,
dashboardId: this.dashboardId,
onDashboardSelected: (dashboardId) => {
overlayRef.dispose();
this.dashboardId = dashboardId;
this.updateView();
}
}
},
{
provide: OverlayRef,
useValue: overlayRef
}
}
);
overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
}
private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): Injector {
const providers: StaticProvider[] = [
{provide: DASHBOARD_SELECT_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
}
}
private updateView() {

View File

@ -16,7 +16,7 @@
-->
<mat-form-field [formGroup]="selectEntityFormGroup" class="mat-block" [appearance]="appearance">
<mat-label>{{ entityText | translate }}</mat-label>
<mat-label>{{ label | translate }}</mat-label>
<input matInput type="text"
#entityInput
formControlName="entity"
@ -42,6 +42,6 @@
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectEntityFormGroup.get('entity').hasError('required')">
{{ entityRequiredText | translate }}
{{ requiredErrorText | translate }}
</mat-error>
</mat-form-field>

View File

@ -36,10 +36,10 @@ import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { BaseData } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityService } from '@core/http/entity.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { isEqual } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
@Component({
selector: 'tb-entity-autocomplete',
@ -61,6 +61,22 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
entitySubtypeValue: string;
entityText: string;
noEntitiesMatchingText: string;
entityRequiredText: string;
filteredEntities: Observable<Array<BaseData<EntityId>>>;
searchText = '';
private dirty = false;
private refresh$ = new Subject<Array<BaseData<EntityId>>>();
private propagateChange = (v: any) => { };
@Input()
set entityType(entityType: EntityType) {
if (this.entityTypeValue !== entityType) {
@ -100,16 +116,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
appearance: MatFormFieldAppearance = 'fill';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@coerceBoolean()
required: boolean;
@Input()
@coerceBoolean()
disabled: boolean;
@Output()
@ -117,19 +129,20 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@ViewChild('entityInput', {static: true}) entityInput: ElementRef;
entityText: string;
noEntitiesMatchingText: string;
entityRequiredText: string;
get requiredErrorText(): string {
if (this.requiredText && this.requiredText.length) {
return this.requiredText;
}
return this.entityRequiredText;
}
filteredEntities: Observable<Array<BaseData<EntityId>>>;
get label(): string {
if (this.labelText && this.labelText.length) {
return this.labelText;
}
return this.entityText;
}
searchText = '';
private dirty = false;
private refresh$ = new Subject<Array<BaseData<EntityId>>>();
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
@ -249,12 +262,6 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
break;
}
}
if (this.labelText && this.labelText.length) {
this.entityText = this.labelText;
}
if (this.requiredText && this.requiredText.length) {
this.entityRequiredText = this.requiredText;
}
const currentEntity = this.getCurrentEntity();
if (currentEntity) {
const currentEntityType = currentEntity.id.entityType;
@ -342,12 +349,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
map((data) => {
if (data) {
if (this.excludeEntityIds && this.excludeEntityIds.length) {
const excludeEntityIdsSet = new Set(this.excludeEntityIds);
const entities: Array<BaseData<EntityId>> = [];
data.forEach((entity) => {
if (this.excludeEntityIds.indexOf(entity.id.id) === -1) {
entities.push(entity);
}
});
data.forEach(entity => !excludeEntityIdsSet.has(entity.id.id) && entities.push(entity));
return entities;
} else {
return data;

View File

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block">
<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block" [appearance]="appearance">
<mat-label>{{ entitySubtypeText | translate }}</mat-label>
<input matInput type="text" placeholder="{{ selectEntitySubtypeText | translate }}"
#subTypeInput

View File

@ -37,6 +37,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { AssetService } from '@core/http/asset.service';
import { EntityViewService } from '@core/http/entity-view.service';
import { EdgeService } from '@core/http/edge.service';
import { MatFormFieldAppearance } from '@angular/material/form-field';
@Component({
selector: 'tb-entity-subtype-autocomplete',
@ -71,6 +72,12 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor,
@Input()
disabled: boolean;
@Input()
excludeSubTypes: Array<string>;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef;
selectEntitySubtypeText: string;
@ -238,9 +245,14 @@ export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor,
break;
}
if (subTypesObservable) {
const excludeSubTypesSet = new Set(this.excludeSubTypes);
this.subTypes = subTypesObservable.pipe(
catchError(() => of([] as Array<EntitySubtype>)),
map(subTypes => subTypes.map(subType => subType.type)),
map(subTypes => {
const filteredSubTypes: Array<string> = [];
subTypes.forEach(subType => !excludeSubTypesSet.has(subType.type) && filteredSubTypes.push(subType.type));
return filteredSubTypes;
}),
publishReplay(1),
refCount()
);

View File

@ -18,7 +18,7 @@
<div class="tb-html" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-html-toolbar">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;" class="tb-html-toolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (hasErrors || required && !modelValue), 'tb-required': !disabled && required}">{{ label }}</label>
<span fxFlex></span>
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyHtml()">

View File

@ -23,6 +23,7 @@ $previewSize: 96px !default;
.tb-container {
margin-top: 0;
padding: 0;
label.tb-title {
display: block;
padding-bottom: 8px;

View File

@ -18,7 +18,7 @@
<div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight, 'tb-js-func-title': functionTitle}"
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;" class="tb-js-func-toolbar">
<label *ngIf="functionTitle" class="tb-title no-padding"
[ngClass]="{'tb-error': !disabled && (hasErrors || !functionValid || required && !modelValue), 'tb-required': !disabled && required}">{{functionTitle + ': f(' + functionArgsString + ')' }}</label>
<label *ngIf="!functionTitle" class="tb-title no-padding"

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -30,8 +30,9 @@ import {
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Subscription } from 'rxjs';
import { SubscriptSizing } from '@angular/material/form-field';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-key-val-map',
@ -50,7 +51,7 @@ import { SubscriptSizing } from '@angular/material/form-field';
}
]
})
export class KeyValMapComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator {
export class KeyValMapComponent extends PageComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator {
@Input() disabled: boolean;
@ -67,19 +68,27 @@ export class KeyValMapComponent extends PageComponent implements ControlValueAcc
kvListFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
ngOnInit(): void {
this.kvListFormGroup = this.fb.group({});
this.kvListFormGroup.addControl('keyVals',
this.fb.array([]));
this.kvListFormGroup = this.fb.group({
keyVals: this.fb.array([])
});
this.kvListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
keyValsFormArray(): UntypedFormArray {
@ -103,9 +112,6 @@ export class KeyValMapComponent extends PageComponent implements ControlValueAcc
}
writeValue(keyValMap: {[key: string]: string}): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const keyValsControls: Array<AbstractControl> = [];
if (keyValMap) {
for (const property of Object.keys(keyValMap)) {
@ -117,10 +123,7 @@ export class KeyValMapComponent extends PageComponent implements ControlValueAcc
}
}
}
this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls));
this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls), {emitEvent: false});
if (this.disabled) {
this.kvListFormGroup.disable({emitEvent: false});
} else {

View File

@ -17,7 +17,7 @@
-->
<div class="markdown-content" [ngClass]="{'tb-edit-mode': !readonly}"
tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="markdown-editor-toolbar">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;" class="markdown-editor-toolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && required && !markdownValue, 'tb-required': !disabled && required}">{{ label }}</label>
<span fxFlex></span>
<button [fxShow]="!editorMode"

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
.markdown-content {
min-width: 400px;
min-width: 300px;
&.tb-edit-mode {
.tb-markdown-view-container {
border: 1px solid #c0c0c0;

View File

@ -16,7 +16,7 @@
-->
<section [formGroup]="stringItemsForm">
<mat-form-field fxFlex class="mat-block" [floatLabel]="floatLabel" [appearance]="appearance">
<mat-form-field fxFlex class="mat-block" [floatLabel]="floatLabel" [appearance]="appearance" [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="label">{{ label }}</mat-label>
<mat-chip-grid #itemsChipList formControlName="items" [required]="required">
<mat-chip-row *ngFor="let item of stringItemsList"

View File

@ -18,7 +18,7 @@ import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { FloatLabelType, MatFormFieldAppearance } from '@angular/material/form-field';
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
@Component({
@ -79,6 +79,9 @@ export class StringItemsListComponent implements ControlValueAccessor{
@coerceBoolean()
editable = false;
@Input()
subscriptSizing: SubscriptSizing = 'fixed'
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder) {

View File

@ -4241,7 +4241,6 @@
"maximum-ota-packages-sum-data-size": "Suma màxima de la mida dels fitxers del paquet ota en bytes (0 - il·limitat)",
"maximum-ota-package-sum-data-size-required": "Cal suma màxima de la mida dels fitxers del paquet ota.",
"maximum-ota-package-sum-data-size-range": "Suma màxima de la mida dels fitxers del paquet ota no pot ser negatiu",
"transport-tenant-msg-rate-limit": "Taxa de missatges de transport per propietari.",
"transport-tenant-telemetry-msg-rate-limit": "Taxa de missatges de telemetria per propietari.",
"transport-tenant-telemetry-data-points-rate-limit": "Taxa de punts de dades per propietari.",
"transport-device-msg-rate-limit": "Taxa de missatges de dispositiu.",

View File

@ -2612,7 +2612,6 @@
"maximum-ota-packages-sum-data-size": "Maximální součet velikosti souborů ota balíčků v bajtech (0 - neomezeno)",
"maximum-ota-package-sum-data-size-required": "Maximální součet velikosti souborů ota balíčků je povinný.",
"maximum-ota-package-sum-data-size-range": "Maximální součet velikosti souborů ota balíčků nemůže být záporný",
"transport-tenant-msg-rate-limit": "Limit přenosu zpráv tenanta.",
"transport-tenant-telemetry-msg-rate-limit": "Limit přenosu zpráv telemetrie tenanta.",
"transport-tenant-telemetry-data-points-rate-limit": "Limit přenosu datových bodů telemetrie tenanta.",
"transport-device-msg-rate-limit": "Limit přenosu zpráv zařízení.",

View File

@ -3130,7 +3130,6 @@
"maximum-scheduler-events": "Maks. antal planlægningsbegivenheder (0 ubegrænset)",
"maximum-scheduler-events-required": "Maks. antal planlægningsbegivenheder er påkrævet.",
"maximum-scheduler-events-range": "Maks. antal planlægningsbegivenheder kan ikke være negativt",
"transport-tenant-msg-rate-limit": "Hastighedsgrænse for transport af lejermeddelelser.",
"transport-tenant-telemetry-msg-rate-limit": "Hastighedsgrænse for transport af lejertelemetrimeddelelser.",
"transport-tenant-telemetry-data-points-rate-limit": "Hastighedsgrænse for transport af lejertelemetridatapunkter.",
"transport-device-msg-rate-limit": "Hastighedsgrænse for transport af enhedsmeddelelser.",

View File

@ -341,6 +341,7 @@
"authentication-settings": "Authentication settings",
"auth-method": "Authentication method",
"auth-method-username-password": "Password / access token",
"auth-method-username-password-hint": "GitHub users <b>must</b> use access <a href='https://github.com/settings/tokens' target='_blank'>tokens</a> with write permissions to the repository.",
"auth-method-private-key": "Private key",
"password-access-token": "Password / access token",
"change-password-access-token": "Change password / access token",
@ -2398,6 +2399,7 @@
"ignore-case": "ignore case",
"value": "Value",
"remove-filter": "Remove filter",
"duplicate-filter": "Duplicate filter",
"preview": "Filter preview",
"no-filters": "No filters configured",
"add-filter": "Add filter",
@ -3535,7 +3537,7 @@
"maximum-ota-packages-sum-data-size": "OTA package files sum size",
"maximum-ota-package-sum-data-size-required": "OTA package files sum size is required.",
"maximum-ota-package-sum-data-size-range": "OTA package files sum size can`t be negative",
"transport-tenant-msg-rate-limit": "Transport tenant messages",
"rest-requests-for-tenant": "REST requests for tenant",
"transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages",
"transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points",
"transport-device-msg-rate-limit": "Transport device messages",
@ -3610,7 +3612,7 @@
"edit-transport-device-msg-title": "Edit transport device messages rate limits",
"edit-transport-device-telemetry-msg-title": "Edit transport device telemetry messages rate limits",
"edit-transport-device-telemetry-data-points-title": "Edit transport device telemetry data points rate limits",
"edit-transport-tenant-msg-rate-limit-title": "Edit transport tenant messages rate limits",
"edit-tenant-rest-limits-title": "Edit REST requests for tenant rate limits",
"edit-customer-rest-limits-title": "Edit REST requests for customer rate limits",
"edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits",
"edit-cassandra-tenant-limits-configuration-title": "Edit Cassandra query for tenant rate limits",

View File

@ -3092,7 +3092,6 @@
"maximum-ota-packages-sum-data-size": "Tamaño máximo de paquetes OTA en bytes (0 - sin límite)",
"maximum-ota-package-sum-data-size-required": "Tamaño máximo de paquetes OTA requerido.",
"maximum-ota-package-sum-data-size-range": "Tamaño máximo de paquetes OTA no puede ser negativo",
"transport-tenant-msg-rate-limit": "Tasa de mensajes de transporte por propietario.",
"transport-tenant-telemetry-msg-rate-limit": "Tasa de mensajes de telemetría por propietario.",
"transport-tenant-telemetry-data-points-rate-limit": "Tasa de datapoints por propietario.",
"transport-device-msg-rate-limit": "Tasa de mensajes de dispositivo.",

View File

@ -2047,7 +2047,6 @@
"maximum-rule-chains": "Maximum number of rule chains (0 - unlimited)",
"maximum-rule-chains-required": "Maximum number of rule chains is required.",
"maximum-rule-chains-range": "Maximum number of rule chains can't be negative",
"transport-tenant-msg-rate-limit": "Transport tenant messages rate limit.",
"transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages rate limit.",
"transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points rate limit.",
"transport-device-msg-rate-limit": "Transport device messages rate limit.",

View File

@ -2047,7 +2047,6 @@
"maximum-rule-chains": "Maximum number of rule chains (0 - unlimited)",
"maximum-rule-chains-required": "Maximum number of rule chains is required.",
"maximum-rule-chains-range": "Maximum number of rule chains can't be negative",
"transport-tenant-msg-rate-limit": "Transport tenant messages rate limit.",
"transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages rate limit.",
"transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points rate limit.",
"transport-device-msg-rate-limit": "Transport device messages rate limit.",

View File

@ -2631,7 +2631,6 @@
"maximum-ota-packages-sum-data-size": "Ota paketi dosyalarının bayt cinsinden maksimum toplamı (0 - sınırsız)",
"maximum-ota-package-sum-data-size-required": "Ota paketi dosyalarının maksimum toplamı gerekli.",
"maximum-ota-package-sum-data-size-range": "Ota paketi dosyalarının maksimum toplamı negatif olamaz",
"transport-tenant-msg-rate-limit": "Taşıma tenant mesajları hız sınırı.",
"transport-tenant-telemetry-msg-rate-limit": "Taşıma tenant telemetri iletileri hız sınırı.",
"transport-tenant-telemetry-data-points-rate-limit": "Taşıma tenant telemetri veri noktaları hız sınırı.",
"transport-device-msg-rate-limit": "Taşıma cihazı mesajları hız sınırı.",

View File

@ -3213,7 +3213,6 @@
"maximum-ota-packages-sum-data-size": "OTA包文件总大小",
"maximum-ota-package-sum-data-size-required": "OTA包文件总大小必填。",
"maximum-ota-package-sum-data-size-range": "OTA包文件总大小不能为负数",
"transport-tenant-msg-rate-limit": "租户消息",
"transport-tenant-telemetry-msg-rate-limit": "租户遥测消息",
"transport-tenant-telemetry-data-points-rate-limit": "租户遥测数据点",
"transport-device-msg-rate-limit": "设备消息",
@ -3287,7 +3286,6 @@
"edit-transport-device-msg-title": "编辑传输设备消息速率限制",
"edit-transport-device-telemetry-msg-title": "编辑传输设备遥测消息速率限制",
"edit-transport-device-telemetry-data-points-title": "编辑传输设备遥测数据点速率限制",
"edit-transport-tenant-msg-rate-limit-title": "编辑传输租户消息速率限制",
"edit-customer-rest-limits-title": "编辑客户REST请求速率限制",
"edit-ws-limit-updates-per-session-title": "编辑会话WS更新速率限制",
"edit-cassandra-tenant-limits-configuration-title": "编辑租户Cassandra查询速率限制",

View File

@ -3063,7 +3063,6 @@
"maximum-ota-packages-sum-data-size": "OTA套件檔尺寸總計",
"maximum-ota-package-sum-data-size-required": "需要OTA套件檔總和大小。",
"maximum-ota-package-sum-data-size-range": "OTA套件檔尺寸總計不可為否",
"transport-tenant-msg-rate-limit": "傳輸租戶訊息",
"transport-tenant-telemetry-msg-rate-limit": "傳輸租戶遙測訊息",
"transport-tenant-telemetry-data-points-rate-limit": "傳輸租戶遙測資料端",
"transport-device-msg-rate-limit": "傳輸設備訊息",
@ -3137,7 +3136,6 @@
"edit-transport-device-msg-title": "編輯傳輸設備訊息速率限制",
"edit-transport-device-telemetry-msg-title": "編輯傳輸設備遙測訊息速率限制",
"edit-transport-device-telemetry-data-points-title": "編輯傳輸設備遙測資料端速率限制",
"edit-transport-tenant-msg-rate-limit-title": "編輯傳輸租戶訊息速率限制",
"edit-customer-rest-limits-title": "編輯剩餘顧客速率限制",
"edit-ws-limit-updates-per-session-title": "編輯每個對談的 WS更新速率限制",
"edit-cassandra-tenant-limits-configuration-title": "編輯租戶速率限制的 Cassandra 查詢",

View File

@ -113,6 +113,10 @@ fieldset {
border: none;
}
.fields-group {
display: grid;
}
section.tb-header-buttons {
position: absolute;
top: 86px;