Browse Source

Merge branch 'master' into add-tooltip-entity-version-restore-dialog

pull/14058/head
Vladyslav Prykhodko 10 months ago
committed by GitHub
parent
commit
6ee3a062b6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  2. 131
      ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html
  3. 100
      ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html
  4. 158
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html
  5. 4
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts
  6. 127
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html
  7. 33
      ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html
  8. 100
      ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html
  9. 2
      ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html
  10. 2
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  11. 3
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss
  12. 32
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts
  13. 11
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts
  14. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts
  15. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts
  16. 2
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  17. 2
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  18. 21
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  19. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
  20. 7
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
  21. 15
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts
  22. 40
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  23. 57
      ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html
  24. 89
      ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts
  25. 2
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  26. 29
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts
  27. 5
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html
  28. 5
      ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts
  29. 7
      ui-ngx/src/app/shared/models/time/time.models.ts
  30. 88
      ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts
  31. 62
      ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts
  32. 35
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  33. 4
      ui-ngx/src/app/shared/models/widget/widget-model.definition.ts
  34. 2440
      ui-ngx/src/assets/dashboard/api_usage.json
  35. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
ui-ngx/src/app/modules/home/components/relation/relation-table.component.html

@ -117,9 +117,13 @@
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toName">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.to-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
<a class="link" [routerLink]="relation.entityURL">
{{ relation.toName | customTranslate }}
</a>
@if(relation.entityURL){
<a class="link" [routerLink]="relation.entityURL">
{{ relation.toName | customTranslate }}
</a>
} @else {
{{ relation.toName | customTranslate }}
}
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName">

131
ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html

@ -15,76 +15,79 @@
limitations under the License.
-->
<section *ngIf="!versionCreateResult$" class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.create-entities-version' | translate }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="createVersionFormGroup" class="flex flex-col" style="flex: 1; padding-top: 16px; overflow: auto;">
<div class="flex flex-col gt-sm:flex-row gt-sm:gap-2">
<tb-branch-autocomplete
class="flex-1"
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block flex-1">
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
@if (!versionCreateResult$) {
<section class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.create-entities-version' | translate }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="createVersionFormGroup" class="flex flex-col" style="flex: 1; padding-top: 16px; overflow: auto;">
<div class="flex flex-col gt-sm:flex-row gt-sm:gap-2">
<tb-branch-autocomplete
class="flex-1"
appearance="outline"
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error [class.hidden]="!(createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern'))">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field class="mat-block" style="margin-bottom: 24px;" appearance="outline">
<mat-label translate>version-control.default-sync-strategy</mat-label>
<mat-select required formControlName="syncStrategy">
@for (strategy of syncStrategies; track strategy) {
<mat-option [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
}
</mat-select>
<mat-hint [innerHTML]="(syncStrategyHints.get(createVersionFormGroup.get('syncStrategy').value) | translate) | safe:'html'"></mat-hint>
</mat-form-field>
<tb-entity-types-version-create
formControlName="entityTypes">
</tb-entity-types-version-create>
</form>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || createVersionFormGroup.invalid || !createVersionFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
<mat-form-field class="mat-block" style="margin-bottom: 24px;">
<mat-label translate>version-control.default-sync-strategy</mat-label>
<mat-select required formControlName="syncStrategy">
<mat-option *ngFor="let strategy of syncStrategies" [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
</mat-select>
<mat-hint [innerHTML]="(syncStrategyHints.get(createVersionFormGroup.get('syncStrategy').value) | translate) | safe:'html'"></mat-hint>
</mat-form-field>
<tb-entity-types-version-create
formControlName="entityTypes">
</tb-entity-types-version-create>
</form>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || createVersionFormGroup.invalid || !createVersionFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="versionCreateResult$">
<section *ngIf="resultMessage">
<div class="mat-headline-6 vc-result-message" [innerHtml]="resultMessage"></div>
</section>
<div *ngIf="(versionCreateResult$ | async)?.done || hasError; else progress" class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
<ng-template #progress>
} @else {
<div class="mat-headline-6 vc-result-message" [class.hidden]="!resultMessage" [innerHtml]="resultMessage"></div>
@if ((versionCreateResult$ | async)?.done || hasError) {
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

100
ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html

@ -15,59 +15,63 @@
limitations under the License.
-->
<section *ngIf="!versionLoadResult$" class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="loadVersionFormGroup" class="flex flex-col" style="flex: 1; padding: 16px 0; overflow-y: auto; overflow-x: hidden">
<tb-entity-types-version-load
formControlName="entityTypes">
</tb-entity-types-version-load>
</form>
<div tb-hint-tooltip-icon="{{ 'version-control.rollback-on-error-hint' | translate }}" [formGroup]="loadVersionFormGroup">
<mat-slide-toggle formControlName="rollbackOnError">
{{ 'version-control.rollback-on-error' | translate }}
</mat-slide-toggle>
</div>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || loadVersionFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>
<section *ngIf="versionLoadResult$">
<div *ngIf="(versionLoadResult$ | async)?.done && !entityTypeLoadResults?.length && !errorMessage" class="mat-headline-6 vc-result-message">
@if (!versionLoadResult$) {
<section class="flex flex-col" style="height: 100%;">
<mat-toolbar>
<h2>{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="loadVersionFormGroup" class="flex flex-col" style="flex: 1; padding: 16px 0; overflow-y: auto; overflow-x: hidden">
<tb-entity-types-version-load
formControlName="entityTypes">
</tb-entity-types-version-load>
</form>
<div tb-hint-tooltip-icon="{{ 'version-control.rollback-on-error-hint' | translate }}" [formGroup]="loadVersionFormGroup">
<mat-slide-toggle formControlName="rollbackOnError">
{{ 'version-control.rollback-on-error' | translate }}
</mat-slide-toggle>
</div>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || loadVersionFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>
} @else {
<div [class.hidden]="!((versionLoadResult$ | async)?.done && !entityTypeLoadResults?.length && !errorMessage)" class="mat-headline-6 vc-result-message">
{{ 'version-control.no-entities-restored' | translate }}
</div>
<div *ngIf="errorMessage" class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div>
<div *ngFor="let entityTypeLoadResult of entityTypeLoadResults" class="mat-headline-6 vc-result-message">{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}</div>
<div *ngIf="(versionLoadResult$ | async)?.done || hasError; else progress" class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
<ng-template #progress>
<div [class.hidden]="!errorMessage" class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div>
@for (entityTypeLoadResult of entityTypeLoadResults; track entityTypeLoadResult.entityType) {
<div class="mat-headline-6 vc-result-message">{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}</div>
}
@if ((versionLoadResult$ | async)?.done || hasError) {
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.restoring-entities-from-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

158
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html

@ -18,86 +18,90 @@
<fieldset [formGroup]="entityTypesVersionCreateFormGroup" class="fields-group flex flex-col">
<legend class="group-title" translate>version-control.entities-to-export</legend>
<div class="flex flex-col" style="flex: 1; overflow: auto;">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
let $index = index; last as isLast;"
class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-4">
<tb-entity-type-select
showLabel
formControlName="entityType"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4" formGroupName="config">
<mat-form-field class="mat-block flex-1">
<mat-label translate>version-control.sync-strategy</mat-label>
<mat-select formControlName="syncStrategy">
<mat-option [value]="'default'">
{{ 'version-control.default' | translate }}
</mat-option>
<mat-option *ngFor="let strategy of syncStrategies" [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<div class="flex flex-1 flex-col">
<mat-checkbox *ngIf="entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE" formControlName="saveCredentials">
{{ 'version-control.export-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveAttributes">
{{ 'version-control.export-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
@for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) {
<div class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button [class.hidden]="disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType(index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-4">
<tb-entity-type-select
showLabel
formControlName="entityType"
appearance="outline"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4" formGroupName="config">
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>version-control.sync-strategy</mat-label>
<mat-select formControlName="syncStrategy">
<mat-option [value]="'default'">
{{ 'version-control.default' | translate }}
</mat-option>
@for (strategy of syncStrategies; track strategy) {
<mat-option [value]="strategy">
{{syncStrategyTranslations.get(strategy) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="flex flex-1 flex-col">
<mat-checkbox [class.hidden]="entityTypeFormGroup.get('entityType').value !== entityTypes.DEVICE" formControlName="saveCredentials">
{{ 'version-control.export-credentials' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveAttributes">
{{ 'version-control.export-attributes' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div>
</div>
</div>
<div class="flex flex-row xs:flex-col gt-xs:min-h-19 gt-xs:items-center gt-xs:justify-start gt-xs:gap-4"
formGroupName="config">
<mat-slide-toggle formControlName="allEntities">
{{ 'version-control.all-entities' | translate }}
</mat-slide-toggle>
@if (!entityTypeFormGroup.get('config').get('allEntities').value) {
<tb-entity-list
class="flex-1"
appearance="outline"
subscriptSizing="dynamic"
[entityType]="entityTypeFormGroup.get('entityType').value"
required
formControlName="entityIds">
</tb-entity-list>
}
</div>
</div>
<div class="flex flex-row xs:flex-col gt-xs:min-h-19 gt-xs:items-center gt-xs:justify-start gt-xs:gap-4"
formGroupName="config">
<mat-slide-toggle formControlName="allEntities">
{{ 'version-control.all-entities' | translate }}
</mat-slide-toggle>
<tb-entity-list
class="flex-1"
[class.!hidden]="entityTypeFormGroup.get('config').get('allEntities').value"
[entityType]="entityTypeFormGroup.get('entityType').value"
required
formControlName="entityIds">
</tb-entity-list>
</div>
</div>
</mat-expansion-panel>
</div>
<div *ngIf="!entityTypesFormGroupArray().length">
<span translate
class="tb-prompt flex items-center justify-center">version-control.no-entities-to-export-prompt</span>
</div>
</mat-expansion-panel>
</div>
} @empty {
<span translate class="tb-prompt flex items-center justify-center">version-control.no-entities-to-export-prompt</span>
}
</div>
<div style="padding-top: 16px;" class="flex flex-row">
<button mat-raised-button color="primary"

4
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts

@ -186,10 +186,6 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
return !!(entityTypeControl as any).expanded;
}
public trackByEntityType(index: number, entityTypeControl: AbstractControl): any {
return entityTypeControl;
}
public removeEntityType(index: number) {
(this.entityTypesVersionCreateFormGroup.get('entityTypes') as UntypedFormArray).removeAt(index);
}

127
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html

@ -18,73 +18,72 @@
<fieldset class="fields-group flex flex-col" [formGroup]="entityTypesVersionLoadFormGroup">
<legend class="group-title" translate>version-control.entities-to-restore</legend>
<div class="flex flex-col" style="flex: 1; overflow: auto;">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType;
let $index = index; last as isLast;"
class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-8">
<tb-entity-type-select
class="flex-1"
showLabel
formControlName="entityType"
required
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col xs:gap-2 gt-xs:gap-4" formGroupName="config">
<div class="flex flex-1 flex-col">
<mat-checkbox #removeOtherEntitiesCheckbox
formControlName="removeOtherEntities"
(change)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup)">
{{ 'version-control.remove-other-entities' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="findExistingEntityByName">
{{ 'version-control.find-existing-entity-by-name' | translate }}
</mat-checkbox>
</div>
<div class="flex flex-1 flex-col">
<mat-checkbox *ngIf="entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE" formControlName="loadCredentials">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadAttributes">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
@for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) {
<div class="flex flex-row items-center justify-start" [class.pb-2]="!isLast">
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup"
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
<mat-expansion-panel-header>
<div class="flex flex-1 flex-row items-center justify-start">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-start">
<div>{{ entityTypeText(entityTypeFormGroup) }}</div>
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button [class.hidden]="disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEntityType(index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div class="entity-type-config-content flex flex-col gap-2">
<mat-divider></mat-divider>
<div class="flex flex-row xs:flex-col gt-xs:gap-8">
<tb-entity-type-select
class="flex-1"
showLabel
formControlName="entityType"
required
appearance="outline"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col xs:gap-2 gt-xs:gap-4" formGroupName="config">
<div class="flex flex-1 flex-col">
<mat-checkbox #removeOtherEntitiesCheckbox
formControlName="removeOtherEntities"
(change)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup)">
{{ 'version-control.remove-other-entities' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="findExistingEntityByName">
{{ 'version-control.find-existing-entity-by-name' | translate }}
</mat-checkbox>
</div>
<div class="flex flex-1 flex-col">
<mat-checkbox [class.hidden]="entityTypeFormGroup.get('entityType').value !== entityTypes.DEVICE" formControlName="loadCredentials">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadAttributes">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<div *ngIf="!entityTypesFormGroupArray().length">
<span translate
class="tb-prompt flex items-center justify-center">version-control.no-entities-to-restore-prompt</span>
</div>
</mat-expansion-panel>
</div>
} @empty {
<span translate class="tb-prompt flex items-center justify-center">version-control.no-entities-to-restore-prompt</span>
}
</div>
<div style="padding-top: 16px;" class="flex flex-row">
<button mat-raised-button color="primary"

33
ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.html

@ -15,39 +15,40 @@
limitations under the License.
-->
<section *ngIf="!versionCreateResult$">
@if (!versionCreateResult$) {
<mat-toolbar>
<h2>{{ 'version-control.create-entity-version' | translate }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="createVersionFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<tb-branch-autocomplete
required
appearance="outline"
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block flex-1">
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern')">
<mat-error [class.hidden]="!(createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern'))">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox *ngIf="entityId.entityType === entityTypes.DEVICE" formControlName="saveCredentials" style="margin-bottom: 16px;">
<mat-checkbox [class.hidden]="entityId.entityType !== entityTypes.DEVICE" formControlName="saveCredentials" style="margin-bottom: 16px;">
{{ 'version-control.export-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveAttributes" style="margin-bottom: 16px;">
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveAttributes" style="margin-bottom: 16px;">
{{ 'version-control.export-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveRelations" style="margin-bottom: 16px;">
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityId.entityType)" formControlName="saveCalculatedFields" class="mb-4">
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityId.entityType)" formControlName="saveCalculatedFields" class="mb-4">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div>
@ -67,10 +68,9 @@
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="versionCreateResult$">
<section *ngIf="(versionCreateResult$ | async)?.done || resultMessage; else progress">
<section *ngIf="resultMessage">
} @else {
@if ((versionCreateResult$ | async)?.done || resultMessage) {
<section [class.hidden]="!resultMessage">
<div class="mat-headline-6 vc-result-message">{{ resultMessage }}</div>
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
@ -81,13 +81,12 @@
</button>
</div>
</section>
</section>
<ng-template #progress>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

100
ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html

@ -15,51 +15,54 @@
limitations under the License.
-->
<section *ngIf="!versionLoadResult$">
<mat-toolbar *ngIf="entityDataInfo">
<h2 tbTruncateWithTooltip>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="entityDataInfo && (isLoading$ | async)">
</mat-progress-bar>
<mat-spinner *ngIf="!entityDataInfo" mode="indeterminate" diameter="80"></mat-spinner>
<form *ngIf="entityDataInfo" [formGroup]="restoreFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<mat-checkbox *ngIf="entityDataInfo.hasCredentials" formControlName="loadCredentials" style="margin-bottom: 16px;">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasAttributes" formControlName="loadAttributes" style="margin-bottom: 16px;">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div *ngIf="entityDataInfo" class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || restoreFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>
<section *ngIf="versionLoadResult$">
<section *ngIf="(versionLoadResult$ | async)?.done || errorMessage; else progress">
<section *ngIf="errorMessage">
@if (!versionLoadResult$) {
@if (entityDataInfo) {
<mat-toolbar>
<h2 tbTruncateWithTooltip>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<span class="flex-1"></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
[class.hidden]="(isLoading$ | async) === false">
</mat-progress-bar>
<form [formGroup]="restoreFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<mat-checkbox [class.hidden]="!entityDataInfo?.hasCredentials" formControlName="loadCredentials" style="margin-bottom: 16px;">
{{ 'version-control.load-credentials' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!entityDataInfo?.hasAttributes" formControlName="loadAttributes" style="margin-bottom: 16px;">
{{ 'version-control.load-attributes' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!entityDataInfo?.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!entityDataInfo?.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || restoreFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
} @else {
<mat-spinner mode="indeterminate" diameter="80"></mat-spinner>
}
} @else {
@if ((versionLoadResult$ | async)?.done || errorMessage) {
<section [class.hidden]="!errorMessage">
<div class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div>
<div class="flex items-center justify-end gap-2">
<button mat-button color="primary"
@ -70,13 +73,12 @@
</button>
</div>
</section>
</section>
<ng-template #progress>
} @else {
<section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.restoring-entity-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</section>
</ng-template>
</section>
}
}

2
ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html

@ -19,7 +19,7 @@
<form [formGroup]="confirmFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="(isLoading$ | async)">
<div style="user-select: none;" [innerHTML]="removeOtherEntitiesConfirmText"></div>
<mat-form-field class="mat-block">
<mat-form-field class="mat-block" appearance="outline">
<input matInput formControlName="verification"/>
</mat-form-field>
</fieldset>

2
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -57,7 +57,7 @@
<mat-option *ngFor="let column of configuredColumns; let $index = index"
[value]="$index"
[disabled]="usedCellClickColumns.includes($index)">
{{ getCellClickColumnInfo($index, column) }}
{{ getCellClickColumnInfo($index, column) | customTranslate }}
</mat-option>
</mat-select>
<mat-icon matSuffix

3
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss

@ -50,6 +50,7 @@ $warning-color: #FAA405;
color: $enabled-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($enabled-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$enabled-color};
}
}
@ -58,6 +59,7 @@ $warning-color: #FAA405;
color: $disabled-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($disabled-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$disabled-color};
}
}
@ -66,6 +68,7 @@ $warning-color: #FAA405;
color: $warning-color;
}
.mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($warning-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$warning-color};
}
}

32
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts

@ -20,16 +20,16 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi
import { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { DataKey, DatasourceType, widgetType } from "@shared/models/widget.models";
import { WidgetSubscriptionOptions } from "@core/api/widget-api.models";
import { formattedDataFormDatasourceData } from "@core/utils";
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { formattedDataFormDatasourceData } from '@core/utils';
import { UtilsService } from "@core/services/utils.service";
import { UtilsService } from '@core/services/utils.service';
import {
ApiUsageDataKeysSettings,
apiUsageDefaultSettings,
ApiUsageWidgetSettings
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models";
ApiUsageWidgetSettings,
getUniqueDataKeys
} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
@Component({
selector: 'tb-api-usage-widget',
@ -80,7 +80,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
type: DatasourceType.entity,
name: '',
entityAliasId: this.settings.dsEntityAliasId,
dataKeys: this.getUniqueDataKeys(this.settings.dataKeys)
dataKeys: getUniqueDataKeys(this.settings.apiUsageDataKeys)
}
const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = {
@ -122,7 +122,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
}
parseApiUsages() {
this.settings.dataKeys.forEach((key) => {
this.settings.apiUsageDataKeys.forEach((key) => {
this.apiUsages.push({
label: this.utils.customTranslation(key.label, key.label),
state: key.state,
@ -134,20 +134,6 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
})
}
getUniqueDataKeys(data: ApiUsageDataKeysSettings[]): DataKey[] {
const seenNames = new Set<string>();
return data
.flatMap(item => [item.status, item.maxLimit, item.current])
.filter(key => {
if (seenNames.has(key.name)) {
return false;
}
seenNames.add(key.name);
return true;
});
};
ngOnDestroy() {
if (this.contentResize$) {
this.contentResize$.disconnect();

11
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts

@ -510,11 +510,9 @@ export class LiquidLevelWidgetComponent implements OnInit {
let content: string;
let container: JQuery<HTMLElement>;
const jQueryContainerElement = $(this.liquidLevelContent.nativeElement);
let value = 'N/A';
let value: number | string = 'N/A';
if (isNumeric(data)) {
value = this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from))
.toFixed(this.settings.decimals || 0);
value = +this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0);
}
this.valueColor.update(value);
const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont),
@ -528,10 +526,9 @@ export class LiquidLevelWidgetComponent implements OnInit {
let volume: number | string;
if (this.widgetUnits !== CapacityUnits.percent) {
const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to);
volume = this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from))
.toFixed(this.settings.decimals || 0);
volume = +this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0);
} else {
volume = this.widgetUnitsConvertor(this.volume).toFixed(this.settings.decimals || 0);
volume = +this.widgetUnitsConvertor(this.volume).toFixed(this.ctx.widgetConfig.decimals || 0);
}
const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont),

24
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts

@ -17,10 +17,10 @@
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { DataKey, Widget, widgetType } from '@shared/models/widget.models';
import { Observable } from "rxjs";
import { BackgroundSettings, BackgroundType } from "@shared/models/widget-settings.models";
import { DataKeyType } from "@shared/models/telemetry/telemetry.models";
import { materialColors } from "@shared/models/material.models";
import { Observable } from 'rxjs';
import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { materialColors } from '@shared/models/material.models';
export interface ApiUsageSettingsContext {
aliasController: IAliasController;
@ -33,7 +33,7 @@ export interface ApiUsageSettingsContext {
export interface ApiUsageWidgetSettings {
dsEntityAliasId: string;
dataKeys: ApiUsageDataKeysSettings[];
apiUsageDataKeys: ApiUsageDataKeysSettings[];
targetDashboardState: string;
background: BackgroundSettings;
padding: string;
@ -80,7 +80,7 @@ const generateDataKey = (label: string, status: string, maxLimit: string, curren
export const apiUsageDefaultSettings: ApiUsageWidgetSettings = {
dsEntityAliasId: '',
dataKeys: [
apiUsageDataKeys: [
generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'),
generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'),
generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'),
@ -104,3 +104,15 @@ export const apiUsageDefaultSettings: ApiUsageWidgetSettings = {
padding: '0'
};
export const getUniqueDataKeys = (data: ApiUsageDataKeysSettings[]): DataKey[] => {
const seenNames = new Set<string>();
return data
.flatMap(item => [item.status, item.maxLimit, item.current])
.filter(key => {
if (seenNames.has(key.name)) {
return false;
}
seenNames.add(key.name);
return true;
});
};

24
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts

@ -37,15 +37,15 @@ import {
ApiUsageDataKeysSettings,
apiUsageDefaultSettings,
ApiUsageSettingsContext
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models";
import { deepClone } from "@core/utils";
import { Observable, of } from "rxjs";
} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
import { deepClone } from '@core/utils';
import { Observable, of } from 'rxjs';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from "@home/components/widget/lib/settings/common/key/data-key-config-dialog.component";
import { MatDialog } from "@angular/material/dialog";
import { CdkDragDrop } from "@angular/cdk/drag-drop";
} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
@Component({
selector: 'tb-api-usage-widget-settings',
@ -82,11 +82,11 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
}
protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) {
settingsForm.setControl('dataKeys', this.prepareDataKeysFormArray(settings?.dataKeys), {emitEvent: false});
settingsForm.setControl('apiUsageDataKeys', this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), {emitEvent: false});
}
dataKeysFormArray(): UntypedFormArray {
return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray;
return this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray;
}
trackByDataKey(index: number): any {
@ -104,7 +104,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
}
removeDataKey(index: number) {
(this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray).removeAt(index);
(this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray).removeAt(index);
}
addDataKey() {
@ -115,7 +115,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
maxLimit: null,
current: null
};
const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray;
const dataKeysArray = this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray;
const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]);
dataKeysArray.push(dataKeyControl);
}
@ -131,7 +131,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
return {
dsEntityAliasId: settings?.dsEntityAliasId,
dataKeys: settings?.dataKeys,
apiUsageDataKeys: settings?.apiUsageDataKeys,
targetDashboardState: settings?.targetDashboardState,
background: settings?.background,
padding: settings.padding
@ -141,7 +141,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected onSettingsSet(settings: WidgetSettings) {
this.apiUsageWidgetSettingsForm = this.fb.group({
dsEntityAliasId: [settings?.dsEntityAliasId],
dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys),
apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys),
targetDashboardState: [settings?.targetDashboardState],
background: [settings?.background, []],
padding: [settings.padding, []]

2
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -47,7 +47,7 @@
<table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"
matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear>
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'">
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.table.timestamp-column-name' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex) | async"
[style]="cellStyle(source, null, 0, row, row[0], rowIndex) | async">

2
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -513,7 +513,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
let title = '';
const header = this.sources[index].header.find(column => column.index.toString() === value);
if (value === '0') {
title = 'Timestamp';
title = this.translate.instant('widgets.table.timestamp-column-name');
} else if (value === 'actions') {
title = 'Actions';
} else {

21
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -926,7 +926,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
if (this.modelValue) {
const configuredColumns = new Array<CellClickColumnInfo>();
if (this.modelValue.config?.datasources[0]?.dataKeys?.length) {
configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys));
const {
displayEntityLabel,
displayEntityName,
displayEntityType,
entityNameColumnTitle,
entityLabelColumnTitle
} = this.modelValue.config.settings;
const displayEntitiesArray = [];
if (isDefined(displayEntityName)) {
const displayName = entityNameColumnTitle ? entityNameColumnTitle : 'entityName';
displayEntitiesArray.push({name: displayName, label: displayName});
}
if (isDefined(displayEntityLabel)) {
const displayLabel = entityLabelColumnTitle ? entityLabelColumnTitle : 'entityLabel';
displayEntitiesArray.push({name: displayLabel, label: displayLabel});
}
if (isDefined(displayEntityType)) {
displayEntitiesArray.push({name: 'entityType', label: 'entityType'});
}
configuredColumns.push(...displayEntitiesArray, ...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys));
}
if (this.modelValue.config?.alarmSource?.dataKeys?.length) {
configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.alarmSource.dataKeys));

6
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html

@ -28,6 +28,12 @@
[class.!hidden]="isEdit || dashboardScope !== 'tenant'">
{{'dashboard.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'import')"
[class.!hidden]="isEdit || dashboardScope !== 'tenant'">
{{'dashboard.update-new-version' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')"

7
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts

@ -22,7 +22,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import {
Dashboard,
Dashboard, DashboardInfo,
getDashboardAssignedCustomersText,
isCurrentPublicDashboardCustomer,
isPublicDashboard
@ -31,13 +31,14 @@ import { DashboardService } from '@core/http/dashboard.service';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isEqual } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models';
import { PageLink } from "@shared/models/page/page-link";
@Component({
selector: 'tb-dashboard-form',
templateUrl: './dashboard-form.component.html',
styleUrls: ['./dashboard-form.component.scss']
})
export class DashboardFormComponent extends EntityComponent<Dashboard> {
export class DashboardFormComponent extends EntityComponent<Dashboard, PageLink, DashboardInfo> {
dashboardScope: 'tenant' | 'customer' | 'customer_user' | 'edge';
customerId: string;
@ -117,7 +118,7 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
onPublicLinkCopied($event) {
this.store.dispatch(new ActionNotificationShow(
{
{
message: this.translate.instant('dashboard.public-link-copied-message'),
type: 'success',
duration: 750,

15
ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts

@ -19,18 +19,24 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component';
import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
import {
ManageDashboardCustomersDialogComponent
} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
import { DashboardRoutingModule } from './dashboard-routing.module';
import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import {
MakeDashboardPublicDialogComponent
} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { ImportDashboardFileDialogComponent } from "@home/pages/dashboard/import-dashboard-file-dialog.component";
@NgModule({
declarations: [
DashboardFormComponent,
DashboardTabsComponent,
ManageDashboardCustomersDialogComponent,
MakeDashboardPublicDialogComponent
MakeDashboardPublicDialogComponent,
ImportDashboardFileDialogComponent
],
imports: [
CommonModule,
@ -40,4 +46,5 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
DashboardRoutingModule
]
})
export class DashboardModule { }
export class DashboardModule {
}

40
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -77,11 +77,16 @@ import {
EntityAliasesDialogComponent,
EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component';
import {
DashboardInfoDialogData,
ImportDashboardFileDialogComponent
} from "@home/pages/dashboard/import-dashboard-file-dialog.component";
import { PageLink } from "@shared/models/page/page-link";
@Injectable()
export class DashboardsTableConfigResolver {
export class DashboardsTableConfigResolver {
private readonly config: EntityTableConfig<DashboardInfo | Dashboard> = new EntityTableConfig<DashboardInfo | Dashboard>();
private readonly config: EntityTableConfig<Dashboard, PageLink, DashboardInfo> = new EntityTableConfig<Dashboard, PageLink, DashboardInfo>();
constructor(private store: Store<AppState>,
private dashboardService: DashboardService,
@ -375,7 +380,7 @@ export class DashboardsTableConfigResolver {
return actions;
}
openDashboard($event: Event, dashboard: DashboardInfo) {
openDashboard($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
@ -422,13 +427,27 @@ export class DashboardsTableConfigResolver {
));
}
exportDashboard($event: Event, dashboard: DashboardInfo) {
exportDashboard($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
this.importExport.exportDashboard(dashboard.id.id);
}
importDashboardFile($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
return this.dialog.open<ImportDashboardFileDialogComponent, DashboardInfoDialogData,
boolean>(ImportDashboardFileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dashboard
}
}).afterClosed();
}
addDashboardsToCustomer($event: Event) {
if ($event) {
$event.stopPropagation();
@ -449,7 +468,7 @@ export class DashboardsTableConfigResolver {
});
}
makePublic($event: Event, dashboard: DashboardInfo) {
makePublic($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
@ -470,7 +489,7 @@ export class DashboardsTableConfigResolver {
);
}
makePrivate($event: Event, dashboard: DashboardInfo) {
makePrivate($event: Event, dashboard: Dashboard) {
if ($event) {
$event.stopPropagation();
}
@ -492,7 +511,7 @@ export class DashboardsTableConfigResolver {
);
}
manageAssignedCustomers($event: Event, dashboard: DashboardInfo) {
manageAssignedCustomers($event: Event, dashboard: Dashboard) {
const assignedCustomersIds = dashboard.assignedCustomers ?
dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : [];
this.showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', assignedCustomersIds);
@ -529,7 +548,7 @@ export class DashboardsTableConfigResolver {
});
}
unassignFromCustomer($event: Event, dashboard: DashboardInfo, customerId: string) {
unassignFromCustomer($event: Event, dashboard: Dashboard, customerId: string) {
if ($event) {
$event.stopPropagation();
}
@ -579,7 +598,7 @@ export class DashboardsTableConfigResolver {
);
}
onDashboardAction(action: EntityAction<DashboardInfo>): boolean {
onDashboardAction(action: EntityAction<Dashboard>): boolean {
switch (action.action) {
case 'open':
this.openDashboard(action.event, action.entity);
@ -587,6 +606,9 @@ export class DashboardsTableConfigResolver {
case 'export':
this.exportDashboard(action.event, action.entity);
return true;
case 'import':
this.importDashboardFile(action.event, action.entity);
return true;
case 'makePublic':
this.makePublic(action.event, action.entity);
return true;

57
ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="uploadFileFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<h2>{{ 'dashboard.update-new-version' | translate }}</h2>
<span class="flex-1"></span>
<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>
<tb-file-input [contentConvertFunction]="loadDataFromJsonContent"
[existingFileName]="currentFileName"
(fileNameChanged)="currentFileName = $event"
label="{{'dashboard.upload-file-to-update' | translate}}"
formControlName="file"
dropLabel="{{ 'import.drop-json-file-or' | translate }}"
accept=".json,application/json"
allowedExtensions="json">
</tb-file-input>
</div>
<div mat-dialog-actions class="flex flex-row items-center justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || uploadFileFormGroup.invalid
|| !uploadFileFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

89
ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts

@ -0,0 +1,89 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DashboardService } from '@core/http/dashboard.service';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
export interface DashboardInfoDialogData {
dashboard: Dashboard;
}
@Component({
selector: 'tb-import-dashboard-file-dialog',
templateUrl: './import-dashboard-file-dialog.component.html',
styleUrls: []
})
export class ImportDashboardFileDialogComponent extends DialogComponent<ImportDashboardFileDialogComponent> implements OnInit {
private dashboard: Dashboard;
currentFileName: string = '';
uploadFileFormGroup: FormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData,
private dashboardService: DashboardService,
protected dialogRef: MatDialogRef<ImportDashboardFileDialogComponent>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.dashboard = data.dashboard;
}
ngOnInit(): void {
this.uploadFileFormGroup = this.fb.group({
file: [null]
});
}
cancel(): void {
this.dialogRef.close();
}
save() {
const fileControl = this.uploadFileFormGroup.get('file');
if (!fileControl || !fileControl.value) {
return;
}
const dashboardContent = {
...fileControl.value,
description: this.dashboard.configuration.description
};
this.dashboard.configuration = dashboardContent;
this.dashboardService.saveDashboard(this.dashboard).subscribe(() => {
this.dialogRef.close(true);
})
}
loadDataFromJsonContent(content: string): any {
try {
const importData = JSON.parse(content);
return importData ? importData['configuration'] : importData;
} catch (err) {
this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'}));
return null;
}
}
}

2
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -233,7 +233,7 @@
<div class="notification-content mini" [style.background-color]="preview.processedTemplates.MICROSOFT_TEAMS?.themeColor">
<div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }}
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button.text }}</button>
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button?.text }}</button>
</div>
</section>
</div>

29
ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts

@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TimeService } from '@core/services/time.service';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { isDefined } from '@core/utils';
@Component({
selector: 'tb-datapoints-limit',
@ -69,7 +70,11 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
private propagateChangeValue: any;
private propagateChange = (v: any) => {
this.propagateChangeValue = v;
};
private destroy$ = new Subject<void>();
@ -79,6 +84,9 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (isDefined(this.propagateChangeValue)) {
this.propagateChange(this.propagateChangeValue);
}
}
registerOnTouched(fn: any): void {
@ -115,19 +123,20 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
}
}
private checkLimit(limit?: number): number {
if (!limit || limit < this.minDatapointsLimit()) {
return this.minDatapointsLimit();
writeValue(value: number | null): void {
this.modelValue = value;
let limit = this.modelValue;
if (!limit) {
limit = Math.ceil(this.maxDatapointsLimit() / 2);
} else if (limit < this.minDatapointsLimit()) {
limit = this.minDatapointsLimit();
} else if (limit > this.maxDatapointsLimit()) {
return this.maxDatapointsLimit();
limit = this.maxDatapointsLimit();
}
return limit;
}
writeValue(value: number | null): void {
this.modelValue = this.checkLimit(value);
this.updateView(limit);
this.datapointsLimitFormGroup.patchValue(
{ limit: this.modelValue }, {emitEvent: false}
{ limit: limit }, {emitEvent: false}
);
}

5
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html

@ -15,7 +15,10 @@
limitations under the License.
-->
<mat-form-field [formGroup]="branchFormGroup" class="mat-block" [floatLabel]="(selectionMode || emptyPlaceholder) ? 'always' : 'auto'" subscriptSizing="{{ subscriptSizing }}">
<mat-form-field [formGroup]="branchFormGroup" class="mat-block"
[floatLabel]="(selectionMode || emptyPlaceholder) ? 'always' : 'auto'"
[appearance]="appearance"
[subscriptSizing]="subscriptSizing">
<mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{emptyPlaceholder || ((loading ? 'common.loading' : 'version-control.select-branch') | translate)}}"
#branchInput

5
ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.ts

@ -35,7 +35,7 @@ import { BranchInfo } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { isNotEmptyStr } from '@core/utils';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { SubscriptSizing } from '@angular/material/form-field';
import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field';
@Component({
selector: 'tb-branch-autocomplete',
@ -57,6 +57,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
private requiredValue: boolean;
get required(): boolean {

7
ui-ngx/src/app/shared/models/time/time.models.ts

@ -1125,6 +1125,7 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => {
const noneAggregation = hasAggregation && timewindow.aggregation?.type === AggregationType.NONE;
if (timewindow.selectedTab === TimewindowType.REALTIME) {
if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
delete timewindow.realtime.timewindowMs;
@ -1138,7 +1139,7 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly:
delete timewindow.history?.quickInterval;
delete timewindow.history?.interval;
if (!hasAggregation) {
if (!hasAggregation || noneAggregation) {
delete timewindow.realtime.interval;
}
} else {
@ -1162,13 +1163,15 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly:
delete timewindow.realtime?.quickInterval;
delete timewindow.realtime?.interval;
if (!hasAggregation) {
if (!hasAggregation || noneAggregation) {
delete timewindow.history.interval;
}
}
if (!hasAggregation) {
delete timewindow.aggregation;
} else if (!noneAggregation) {
delete timewindow.aggregation.limit;
}
if (historyOnly) {

88
ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts

@ -0,0 +1,88 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
import {
ApiUsageWidgetSettings,
getUniqueDataKeys
} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
interface AliasFilterPair {
alias?: EntityAliasInfo;
filter?: FilterInfo;
}
interface ApiUsageDatasourcesInfo {
ds?: AliasFilterPair;
}
export const ApiUsageModelDefinition: WidgetModelDefinition<ApiUsageDatasourcesInfo> = {
testWidget(widget: Widget): boolean {
if (widget?.config?.settings) {
const settings = widget.config.settings;
if (settings.apiUsageDataKeys && Array.isArray(settings.apiUsageDataKeys)) {
return true;
}
}
return false;
},
prepareExportInfo(dashboard: Dashboard, widget: Widget): ApiUsageDatasourcesInfo {
const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings;
const info: ApiUsageDatasourcesInfo = {};
if (settings.dsEntityAliasId) {
info.ds = prepareExportDataSourcesInfo(dashboard, settings.dsEntityAliasId);
}
return info;
},
updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: ApiUsageDatasourcesInfo): void {
const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings;
if (info?.ds?.alias) {
settings.dsEntityAliasId = getEntityAliasId(entityAliases, info.ds.alias);
}
},
datasources(widget: Widget): Datasource[] {
const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings;
const datasources: Datasource[] = [];
if (settings.apiUsageDataKeys?.length && settings.dsEntityAliasId) {
datasources.push({
type: DatasourceType.entity,
name: '',
entityAliasId: settings.dsEntityAliasId,
dataKeys: getUniqueDataKeys(settings.apiUsageDataKeys)
});
}
return datasources;
},
hasTimewindow(): boolean {
return false;
}
};
const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: string): AliasFilterPair => {
const aliasAndFilter: AliasFilterPair = {};
const entityAlias = dashboard.configuration.entityAliases[settings];
if (entityAlias) {
aliasAndFilter.alias = {
alias: entityAlias.alias,
filter: entityAlias.filter
};
}
return aliasAndFilter;
}

62
ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts

@ -17,18 +17,15 @@
import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { DataKey, Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models';
import { Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models';
import {
additionalMapDataSourcesToDatasources,
BaseMapSettings,
CirclesDataLayerSettings,
MapDataLayerSettings,
MapDataLayerType,
MapDataSourceSettings,
mapDataSourceSettingsToDatasource,
MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings
MapType
} from '@shared/models/widget/maps/map.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
@ -126,7 +123,7 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
datasources.push(...getMapDataLayersDatasources(settings.circles));
}
if (settings.additionalDataSources?.length) {
datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources));
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));
}
return datasources;
},
@ -137,13 +134,13 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
} else {
const datasources: Datasource[] = [];
if (settings.markers?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.markers, 'markers'));
datasources.push(...getMapDataLayersDatasources(settings.markers, true, 'markers'));
}
if (settings.polygons?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.polygons, 'polygons'));
datasources.push(...getMapDataLayersDatasources(settings.polygons, true, 'polygons'));
}
if (settings.circles?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.circles, 'circles'));
datasources.push(...getMapDataLayersDatasources(settings.circles, true, 'circles'));
}
if (settings.additionalDataSources?.length) {
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));
@ -237,52 +234,21 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource
}
}
const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => {
const getMapDataLayersDatasources = (settings: MapDataLayerSettings[],
includeDataKeys = false, dataLayerType?: MapDataLayerType): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
datasources.push(mapDataSourceSettingsToDatasource(dsSettings));
if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) {
(dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => {
datasources.push(mapDataSourceSettingsToDatasource(ds));
});
}
});
return datasources;
};
const getMapLatestDataLayersDatasources = (settings: MapDataLayerSettings[],
dataLayerType: MapDataLayerType): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
const dataKeys: DataKey[] = getMapLatestDataLayerDatasourceDataKeys(dsSettings, dataLayerType);
const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings);
datasource.dataKeys.push(...dataKeys);
const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings, null, includeDataKeys, dataLayerType);
datasources.push(datasource);
if ((dsSettings).additionalDataSources?.length) {
(dsSettings).additionalDataSources.forEach((ds) => {
if (dsSettings.additionalDataSources?.length) {
dsSettings.additionalDataSources.forEach((ds) => {
const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds);
additionalDatasource.dataKeys.push(...dataKeys);
if (includeDataKeys) {
additionalDatasource.dataKeys.push(...datasource.dataKeys);
}
datasources.push(additionalDatasource);
});
}
});
return datasources;
};
const getMapLatestDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys || [];
switch (dataLayerType) {
case 'markers':
const markersSettings = settings as MarkersDataLayerSettings;
dataKeys.push(markersSettings.xKey, markersSettings.yKey);
break;
case 'polygons':
dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey);
break;
case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break;
}
return dataKeys;
};

35
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -24,6 +24,7 @@ import {
} from '@shared/models/widget.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
deepClone,
guid,
hashCode,
isDefinedAndNotNull,
@ -61,19 +62,44 @@ export interface TbMapDatasource extends Datasource {
mapDataIds: string[];
}
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings, id = guid()): TbMapDatasource => {
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings | MapDataLayerSettings,
id = guid(),
includeDataKeys = false, dataLayerType?: MapDataLayerType): TbMapDatasource => {
const dataKeys = includeDataKeys ? mapDataLayerDatasourceDataKeys((settings as MapDataLayerSettings), dataLayerType) : [];
return {
type: settings.dsType,
name: settings.dsLabel,
deviceId: settings.dsDeviceId,
entityAliasId: settings.dsEntityAliasId,
filterId: settings.dsFilterId,
dataKeys: [],
dataKeys: dataKeys,
latestDataKeys: [],
mapDataIds: [id]
};
};
const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : [];
switch (dataLayerType) {
case 'trips':
const tripsSettings = settings as TripsDataLayerSettings;
dataKeys.push(tripsSettings.xKey, tripsSettings.yKey);
break;
case 'markers':
const markersSettings = settings as MarkersDataLayerSettings;
dataKeys.push(markersSettings.xKey, markersSettings.yKey);
break;
case 'polygons':
dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey);
break;
case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break;
}
return dataKeys;
};
export enum DataLayerPatternType {
pattern = 'pattern',
@ -658,10 +684,11 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings {
dataKeys: DataKey[];
}
export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => {
export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[],
includeDataKeys = true): TbMapDatasource[] => {
return additionalMapDataSources.map(addDs => {
const res = mapDataSourceSettingsToDatasource(addDs);
res.dataKeys = addDs.dataKeys;
res.dataKeys = includeDataKeys ? addDs.dataKeys : [];
return res;
});
};

4
ui-ngx/src/app/shared/models/widget/widget-model.definition.ts

@ -19,6 +19,7 @@ import { Dashboard } from '@shared/models/dashboard.models';
import { EntityAliases } from '@shared/models/alias.models';
import { Filters } from '@shared/models/query/query.models';
import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition';
import { ApiUsageModelDefinition } from '@shared/models/widget/home-widgets/api-usage-model.definition';
export interface WidgetModelDefinition<T = any> {
testWidget(widget: Widget): boolean;
@ -29,7 +30,8 @@ export interface WidgetModelDefinition<T = any> {
}
const widgetModelRegistry: WidgetModelDefinition[] = [
MapModelDefinition
MapModelDefinition,
ApiUsageModelDefinition
];
export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => {

2440
ui-ngx/src/assets/dashboard/api_usage.json

File diff suppressed because it is too large

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

@ -1429,6 +1429,8 @@
"mobile-order": "Dashboard order in mobile application",
"mobile-hide": "Hide dashboard in mobile application",
"update-image": "Update dashboard image",
"update-new-version": "Upload new version",
"upload-file-to-update": "Upload file to update",
"take-screenshot": "Take screenshot",
"select-widget-title": "Select widget",
"select-widget-value": "{{title}}: select widget",
@ -9030,6 +9032,7 @@
"show-empty-space-hidden-action": "Show empty space instead of hidden cell button action",
"dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons",
"display-timestamp": "Timestamp",
"timestamp-column-name":"Timestamp",
"display-pagination": "Display pagination",
"default-page-size": "Default page size",
"page-step-settings": "Page step settings",
@ -9601,7 +9604,8 @@
"add-key": "Add key",
"no-key": "No key",
"delete-key": "Delete key",
"target-dashboard-state": "Target dashboard state"
"target-dashboard-state": "Target dashboard state",
"go-to-main-state": "Go to default view"
}
},
"color": {

Loading…
Cancel
Save