Browse Source

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

pull/14058/head
Vladyslav Prykhodko 1 year 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"> <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-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.to-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation"> <mat-cell *matCellDef="let relation">
<a class="link" [routerLink]="relation.entityURL"> @if(relation.entityURL){
{{ relation.toName | customTranslate }} <a class="link" [routerLink]="relation.entityURL">
</a> {{ relation.toName | customTranslate }}
</a>
} @else {
{{ relation.toName | customTranslate }}
}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName"> <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. limitations under the License.
--> -->
<section *ngIf="!versionCreateResult$" class="flex flex-col" style="height: 100%;"> @if (!versionCreateResult$) {
<mat-toolbar> <section class="flex flex-col" style="height: 100%;">
<h2>{{ 'version-control.create-entities-version' | translate }}</h2> <mat-toolbar>
<span class="flex-1"></span> <h2>{{ 'version-control.create-entities-version' | translate }}</h2>
</mat-toolbar> <span class="flex-1"></span>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate" </mat-toolbar>
*ngIf="isLoading$ | async"> <mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
</mat-progress-bar> [class.hidden]="(isLoading$ | async) === false">
<form [formGroup]="createVersionFormGroup" class="flex flex-col" style="flex: 1; padding-top: 16px; overflow: auto;"> </mat-progress-bar>
<div class="flex flex-col gt-sm:flex-row gt-sm:gap-2"> <form [formGroup]="createVersionFormGroup" class="flex flex-col" style="flex: 1; padding-top: 16px; overflow: auto;">
<tb-branch-autocomplete <div class="flex flex-col gt-sm:flex-row gt-sm:gap-2">
class="flex-1" <tb-branch-autocomplete
required class="flex-1"
formControlName="branch"> appearance="outline"
</tb-branch-autocomplete> required
<mat-form-field class="mat-block flex-1"> formControlName="branch">
<mat-label translate>version-control.version-name</mat-label> </tb-branch-autocomplete>
<input required matInput formControlName="versionName"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required') || <mat-label translate>version-control.version-name</mat-label>
createVersionFormGroup.get('versionName').hasError('pattern')"> <input required matInput formControlName="versionName">
{{ 'version-control.version-name-required' | translate }} <mat-error [class.hidden]="!(createVersionFormGroup.get('versionName').hasError('required') ||
</mat-error> 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> </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> </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> </section>
<div *ngIf="(versionCreateResult$ | async)?.done || hasError; else progress" class="flex items-center justify-end gap-2"> } @else {
<button mat-button color="primary" <div class="mat-headline-6 vc-result-message" [class.hidden]="!resultMessage" [innerHtml]="resultMessage"></div>
type="button" @if ((versionCreateResult$ | async)?.done || hasError) {
[disabled]="(isLoading$ | async)" <div class="flex items-center justify-end gap-2">
(click)="cancel()" cdkFocusInitial> <button mat-button color="primary"
{{ 'action.close' | translate }} type="button"
</button> [disabled]="(isLoading$ | async)"
</div> (click)="cancel()" cdkFocusInitial>
<ng-template #progress> {{ 'action.close' | translate }}
</button>
</div>
} @else {
<section class="flex flex-col items-center justify-center"> <section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress"> <div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.creating-version</span> <span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar> <mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div> </div>
</section> </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. limitations under the License.
--> -->
<section *ngIf="!versionLoadResult$" class="flex flex-col" style="height: 100%;"> @if (!versionLoadResult$) {
<mat-toolbar> <section class="flex flex-col" style="height: 100%;">
<h2>{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}</h2> <mat-toolbar>
<span class="flex-1"></span> <h2>{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}</h2>
</mat-toolbar> <span class="flex-1"></span>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate" </mat-toolbar>
*ngIf="isLoading$ | async"> <mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
</mat-progress-bar> [class.hidden]="(isLoading$ | async) === false">
<form [formGroup]="loadVersionFormGroup" class="flex flex-col" style="flex: 1; padding: 16px 0; overflow-y: auto; overflow-x: hidden"> </mat-progress-bar>
<tb-entity-types-version-load <form [formGroup]="loadVersionFormGroup" class="flex flex-col" style="flex: 1; padding: 16px 0; overflow-y: auto; overflow-x: hidden">
formControlName="entityTypes"> <tb-entity-types-version-load
</tb-entity-types-version-load> formControlName="entityTypes">
</form> </tb-entity-types-version-load>
<div tb-hint-tooltip-icon="{{ 'version-control.rollback-on-error-hint' | translate }}" [formGroup]="loadVersionFormGroup"> </form>
<mat-slide-toggle formControlName="rollbackOnError"> <div tb-hint-tooltip-icon="{{ 'version-control.rollback-on-error-hint' | translate }}" [formGroup]="loadVersionFormGroup">
{{ 'version-control.rollback-on-error' | translate }} <mat-slide-toggle formControlName="rollbackOnError">
</mat-slide-toggle> {{ 'version-control.rollback-on-error' | translate }}
</div> </mat-slide-toggle>
<div class="flex items-center justify-end gap-2" style="padding-top: 16px;"> </div>
<button mat-button color="primary" <div class="flex items-center justify-end gap-2" style="padding-top: 16px;">
type="button" <button mat-button color="primary"
[disabled]="(isLoading$ | async)" type="button"
(click)="cancel()" cdkFocusInitial> [disabled]="(isLoading$ | async)"
{{ 'action.cancel' | translate }} (click)="cancel()" cdkFocusInitial>
</button> {{ 'action.cancel' | translate }}
<button mat-raised-button color="primary" </button>
type="button" <button mat-raised-button color="primary"
(click)="restore()" type="button"
[disabled]="(isLoading$ | async) || loadVersionFormGroup.invalid"> (click)="restore()"
{{ 'action.restore' | translate }} [disabled]="(isLoading$ | async) || loadVersionFormGroup.invalid">
</button> {{ 'action.restore' | translate }}
</div> </button>
</section> </div>
<section *ngIf="versionLoadResult$"> </section>
<div *ngIf="(versionLoadResult$ | async)?.done && !entityTypeLoadResults?.length && !errorMessage" class="mat-headline-6 vc-result-message"> } @else {
<div [class.hidden]="!((versionLoadResult$ | async)?.done && !entityTypeLoadResults?.length && !errorMessage)" class="mat-headline-6 vc-result-message">
{{ 'version-control.no-entities-restored' | translate }} {{ 'version-control.no-entities-restored' | translate }}
</div> </div>
<div *ngIf="errorMessage" class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div> <div [class.hidden]="!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> @for (entityTypeLoadResult of entityTypeLoadResults; track entityTypeLoadResult.entityType) {
<div *ngIf="(versionLoadResult$ | async)?.done || hasError; else progress" class="flex items-center justify-end gap-2"> <div class="mat-headline-6 vc-result-message">{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}</div>
<button mat-button color="primary" }
type="button" @if ((versionLoadResult$ | async)?.done || hasError) {
[disabled]="(isLoading$ | async)" <div class="flex items-center justify-end gap-2">
(click)="cancel()" cdkFocusInitial> <button mat-button color="primary"
{{ 'action.close' | translate }} type="button"
</button> [disabled]="(isLoading$ | async)"
</div> (click)="cancel()" cdkFocusInitial>
<ng-template #progress> {{ 'action.close' | translate }}
</button>
</div>
} @else {
<section class="flex flex-col items-center justify-center"> <section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress"> <div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.restoring-entities-from-version</span> <span translate>version-control.restoring-entities-from-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar> <mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div> </div>
</section> </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"> <fieldset [formGroup]="entityTypesVersionCreateFormGroup" class="fields-group flex flex-col">
<legend class="group-title" translate>version-control.entities-to-export</legend> <legend class="group-title" translate>version-control.entities-to-export</legend>
<div class="flex flex-col" style="flex: 1; overflow: auto;"> <div class="flex flex-col" style="flex: 1; overflow: auto;">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType; @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) {
let $index = index; last as isLast;" <div class="flex flex-row items-center justify-start" [class.pb-2]="!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"
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup" [expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false"> <mat-expansion-panel-header>
<mat-expansion-panel-header> <div class="flex flex-1 flex-row items-center justify-start">
<div class="flex flex-1 flex-row items-center justify-start"> <mat-panel-title>
<mat-panel-title> <div class="flex flex-1 flex-row items-center justify-start">
<div class="flex flex-1 flex-row items-center justify-start"> <div>{{ entityTypeText(entityTypeFormGroup) }}</div>
<div>{{ entityTypeText(entityTypeFormGroup) }}</div> </div>
</div> </mat-panel-title>
</mat-panel-title> <span class="flex-1"></span>
<span class="flex-1"></span> <button [class.hidden]="disabled" mat-icon-button style="min-width: 40px;"
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;" type="button"
type="button" (click)="removeEntityType(index)"
(click)="removeEntityType($index)" matTooltip="{{ 'action.remove' | translate }}"
matTooltip="{{ 'action.remove' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <mat-icon>delete</mat-icon>
<mat-icon>delete</mat-icon> </button>
</button> </div>
</div> </mat-expansion-panel-header>
</mat-expansion-panel-header> <div class="entity-type-config-content flex flex-col gap-2">
<div class="entity-type-config-content flex flex-col gap-2"> <mat-divider></mat-divider>
<mat-divider></mat-divider> <div class="flex flex-row xs:flex-col gt-xs:gap-4">
<div class="flex flex-row xs:flex-col gt-xs:gap-4"> <tb-entity-type-select
<tb-entity-type-select showLabel
showLabel formControlName="entityType"
formControlName="entityType" appearance="outline"
required required
[filterAllowedEntityTypes]="false" [filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)"> [allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select> </tb-entity-type-select>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4" formGroupName="config"> <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-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>version-control.sync-strategy</mat-label> <mat-label translate>version-control.sync-strategy</mat-label>
<mat-select formControlName="syncStrategy"> <mat-select formControlName="syncStrategy">
<mat-option [value]="'default'"> <mat-option [value]="'default'">
{{ 'version-control.default' | translate }} {{ 'version-control.default' | translate }}
</mat-option> </mat-option>
<mat-option *ngFor="let strategy of syncStrategies" [value]="strategy"> @for (strategy of syncStrategies; track strategy) {
{{syncStrategyTranslations.get(strategy) | translate}} <mat-option [value]="strategy">
</mat-option> {{syncStrategyTranslations.get(strategy) | translate}}
</mat-select> </mat-option>
</mat-form-field> }
<div class="flex flex-1 flex-col"> </mat-select>
<mat-checkbox *ngIf="entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE" formControlName="saveCredentials"> </mat-form-field>
{{ 'version-control.export-credentials' | translate }} <div class="flex flex-1 flex-col">
</mat-checkbox> <mat-checkbox [class.hidden]="entityTypeFormGroup.get('entityType').value !== entityTypes.DEVICE" formControlName="saveCredentials">
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveAttributes"> {{ 'version-control.export-credentials' | translate }}
{{ 'version-control.export-attributes' | translate }} </mat-checkbox>
</mat-checkbox> <mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveAttributes">
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations"> {{ 'version-control.export-attributes' | translate }}
{{ 'version-control.export-relations' | translate }} </mat-checkbox>
</mat-checkbox> <mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields"> {{ 'version-control.export-relations' | translate }}
{{ 'version-control.export-calculated-fields' | translate }} </mat-checkbox>
</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> </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>
<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" </mat-expansion-panel>
formGroupName="config"> </div>
<mat-slide-toggle formControlName="allEntities"> } @empty {
{{ 'version-control.all-entities' | translate }} <span translate class="tb-prompt flex items-center justify-center">version-control.no-entities-to-export-prompt</span>
</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>
</div> </div>
<div style="padding-top: 16px;" class="flex flex-row"> <div style="padding-top: 16px;" class="flex flex-row">
<button mat-raised-button color="primary" <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; return !!(entityTypeControl as any).expanded;
} }
public trackByEntityType(index: number, entityTypeControl: AbstractControl): any {
return entityTypeControl;
}
public removeEntityType(index: number) { public removeEntityType(index: number) {
(this.entityTypesVersionCreateFormGroup.get('entityTypes') as UntypedFormArray).removeAt(index); (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"> <fieldset class="fields-group flex flex-col" [formGroup]="entityTypesVersionLoadFormGroup">
<legend class="group-title" translate>version-control.entities-to-restore</legend> <legend class="group-title" translate>version-control.entities-to-restore</legend>
<div class="flex flex-col" style="flex: 1; overflow: auto;"> <div class="flex flex-col" style="flex: 1; overflow: auto;">
<div *ngFor="let entityTypeFormGroup of entityTypesFormGroupArray(); trackBy: trackByEntityType; @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) {
let $index = index; last as isLast;" <div class="flex flex-row items-center justify-start" [class.pb-2]="!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"
<mat-expansion-panel class="entity-type-config flex-1" [class.load]="loading" [formGroup]="entityTypeFormGroup" [expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false">
[expanded]="entityTypesFormGroupExpanded(entityTypeFormGroup)" (opened)="loading = false"> <mat-expansion-panel-header>
<mat-expansion-panel-header> <div class="flex flex-1 flex-row items-center justify-start">
<div class="flex flex-1 flex-row items-center justify-start"> <mat-panel-title>
<mat-panel-title> <div class="flex flex-1 flex-row items-center justify-start">
<div class="flex flex-1 flex-row items-center justify-start"> <div>{{ entityTypeText(entityTypeFormGroup) }}</div>
<div>{{ entityTypeText(entityTypeFormGroup) }}</div> </div>
</div> </mat-panel-title>
</mat-panel-title> <span class="flex-1"></span>
<span class="flex-1"></span> <button [class.hidden]="disabled" mat-icon-button style="min-width: 40px;"
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;" type="button"
type="button" (click)="removeEntityType(index)"
(click)="removeEntityType($index)" matTooltip="{{ 'action.remove' | translate }}"
matTooltip="{{ 'action.remove' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <mat-icon>delete</mat-icon>
<mat-icon>delete</mat-icon> </button>
</button> </div>
</div> </mat-expansion-panel-header>
</mat-expansion-panel-header> <div class="entity-type-config-content flex flex-col gap-2">
<div class="entity-type-config-content flex flex-col gap-2"> <mat-divider></mat-divider>
<mat-divider></mat-divider> <div class="flex flex-row xs:flex-col gt-xs:gap-8">
<div class="flex flex-row xs:flex-col gt-xs:gap-8"> <tb-entity-type-select
<tb-entity-type-select class="flex-1"
class="flex-1" showLabel
showLabel formControlName="entityType"
formControlName="entityType" required
required appearance="outline"
[filterAllowedEntityTypes]="false" [filterAllowedEntityTypes]="false"
[allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)"> [allowedEntityTypes]="allowedEntityTypes(entityTypeFormGroup)">
</tb-entity-type-select> </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-row xs:flex-col xs:gap-2 gt-xs:gap-4" formGroupName="config">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<mat-checkbox #removeOtherEntitiesCheckbox <mat-checkbox #removeOtherEntitiesCheckbox
formControlName="removeOtherEntities" formControlName="removeOtherEntities"
(change)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup)"> (change)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup)">
{{ 'version-control.remove-other-entities' | translate }} {{ 'version-control.remove-other-entities' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox formControlName="findExistingEntityByName"> <mat-checkbox formControlName="findExistingEntityByName">
{{ 'version-control.find-existing-entity-by-name' | translate }} {{ 'version-control.find-existing-entity-by-name' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<mat-checkbox *ngIf="entityTypeFormGroup.get('entityType').value === entityTypes.DEVICE" formControlName="loadCredentials"> <mat-checkbox [class.hidden]="entityTypeFormGroup.get('entityType').value !== entityTypes.DEVICE" formControlName="loadCredentials">
{{ 'version-control.load-credentials' | translate }} {{ 'version-control.load-credentials' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadAttributes"> <mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadAttributes">
{{ 'version-control.load-attributes' | translate }} {{ 'version-control.load-attributes' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations"> <mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations">
{{ 'version-control.load-relations' | translate }} {{ 'version-control.load-relations' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields"> <mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields">
{{ 'version-control.load-calculated-fields' | translate }} {{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox> </mat-checkbox>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </mat-expansion-panel>
</mat-expansion-panel> </div>
</div> } @empty {
<div *ngIf="!entityTypesFormGroupArray().length"> <span translate class="tb-prompt flex items-center justify-center">version-control.no-entities-to-restore-prompt</span>
<span translate }
class="tb-prompt flex items-center justify-center">version-control.no-entities-to-restore-prompt</span>
</div>
</div> </div>
<div style="padding-top: 16px;" class="flex flex-row"> <div style="padding-top: 16px;" class="flex flex-row">
<button mat-raised-button color="primary" <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. limitations under the License.
--> -->
<section *ngIf="!versionCreateResult$"> @if (!versionCreateResult$) {
<mat-toolbar> <mat-toolbar>
<h2>{{ 'version-control.create-entity-version' | translate }}</h2> <h2>{{ 'version-control.create-entity-version' | translate }}</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate" <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> </mat-progress-bar>
<form [formGroup]="createVersionFormGroup" style="padding-top: 16px;"> <form [formGroup]="createVersionFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<tb-branch-autocomplete <tb-branch-autocomplete
required required
appearance="outline"
formControlName="branch"> formControlName="branch">
</tb-branch-autocomplete> </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> <mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName"> <input required matInput formControlName="versionName">
<mat-error *ngIf="createVersionFormGroup.get('versionName').hasError('required') || <mat-error [class.hidden]="!(createVersionFormGroup.get('versionName').hasError('required') ||
createVersionFormGroup.get('versionName').hasError('pattern')"> createVersionFormGroup.get('versionName').hasError('pattern'))">
{{ 'version-control.version-name-required' | translate }} {{ 'version-control.version-name-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </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 }} {{ 'version-control.export-credentials' | translate }}
</mat-checkbox> </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 }} {{ 'version-control.export-attributes' | translate }}
</mat-checkbox> </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 }} {{ 'version-control.export-relations' | translate }}
</mat-checkbox> </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 }} {{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
@ -67,10 +68,9 @@
{{ 'action.create' | translate }} {{ 'action.create' | translate }}
</button> </button>
</div> </div>
</section> } @else {
<section *ngIf="versionCreateResult$"> @if ((versionCreateResult$ | async)?.done || resultMessage) {
<section *ngIf="(versionCreateResult$ | async)?.done || resultMessage; else progress"> <section [class.hidden]="!resultMessage">
<section *ngIf="resultMessage">
<div class="mat-headline-6 vc-result-message">{{ resultMessage }}</div> <div class="mat-headline-6 vc-result-message">{{ resultMessage }}</div>
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<button mat-button color="primary" <button mat-button color="primary"
@ -81,13 +81,12 @@
</button> </button>
</div> </div>
</section> </section>
</section> } @else {
<ng-template #progress>
<section class="flex flex-col items-center justify-center"> <section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress"> <div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.creating-version</span> <span translate>version-control.creating-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar> <mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div> </div>
</section> </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. limitations under the License.
--> -->
<section *ngIf="!versionLoadResult$">
<mat-toolbar *ngIf="entityDataInfo"> @if (!versionLoadResult$) {
<h2 tbTruncateWithTooltip>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2> @if (entityDataInfo) {
<span class="flex-1"></span> <mat-toolbar>
</mat-toolbar> <h2 tbTruncateWithTooltip>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate" <span class="flex-1"></span>
*ngIf="entityDataInfo && (isLoading$ | async)"> </mat-toolbar>
</mat-progress-bar> <mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
<mat-spinner *ngIf="!entityDataInfo" mode="indeterminate" diameter="80"></mat-spinner> [class.hidden]="(isLoading$ | async) === false">
<form *ngIf="entityDataInfo" [formGroup]="restoreFormGroup" style="padding-top: 16px;"> </mat-progress-bar>
<fieldset [disabled]="isLoading$ | async"> <form [formGroup]="restoreFormGroup" style="padding-top: 16px;">
<div class="flex flex-1 flex-col"> <fieldset [disabled]="isLoading$ | async">
<mat-checkbox *ngIf="entityDataInfo.hasCredentials" formControlName="loadCredentials" style="margin-bottom: 16px;"> <div class="flex flex-1 flex-col">
{{ 'version-control.load-credentials' | translate }} <mat-checkbox [class.hidden]="!entityDataInfo?.hasCredentials" formControlName="loadCredentials" style="margin-bottom: 16px;">
</mat-checkbox> {{ 'version-control.load-credentials' | translate }}
<mat-checkbox *ngIf="entityDataInfo.hasAttributes" formControlName="loadAttributes" style="margin-bottom: 16px;"> </mat-checkbox>
{{ 'version-control.load-attributes' | translate }} <mat-checkbox [class.hidden]="!entityDataInfo?.hasAttributes" formControlName="loadAttributes" style="margin-bottom: 16px;">
</mat-checkbox> {{ 'version-control.load-attributes' | translate }}
<mat-checkbox *ngIf="entityDataInfo.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;"> </mat-checkbox>
{{ 'version-control.load-relations' | translate }} <mat-checkbox [class.hidden]="!entityDataInfo?.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;">
</mat-checkbox> {{ 'version-control.load-relations' | translate }}
<mat-checkbox *ngIf="entityDataInfo.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4"> </mat-checkbox>
{{ 'version-control.load-calculated-fields' | translate }} <mat-checkbox [class.hidden]="!entityDataInfo?.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4">
</mat-checkbox> {{ 'version-control.load-calculated-fields' | translate }}
</div> </mat-checkbox>
</fieldset> </div>
</form> </fieldset>
<div *ngIf="entityDataInfo" class="flex items-center justify-end gap-2"> </form>
<button mat-button color="primary" <div class="flex items-center justify-end gap-2">
type="button" <button mat-button color="primary"
[disabled]="(isLoading$ | async)" type="button"
(click)="cancel()" cdkFocusInitial> [disabled]="(isLoading$ | async)"
{{ 'action.cancel' | translate }} (click)="cancel()" cdkFocusInitial>
</button> {{ 'action.cancel' | translate }}
<button mat-raised-button color="primary" </button>
type="button" <button mat-raised-button color="primary"
(click)="restore()" type="button"
[disabled]="(isLoading$ | async) || restoreFormGroup.invalid"> (click)="restore()"
{{ 'action.restore' | translate }} [disabled]="(isLoading$ | async) || restoreFormGroup.invalid">
</button> {{ 'action.restore' | translate }}
</div> </button>
</section> </div>
<section *ngIf="versionLoadResult$"> } @else {
<section *ngIf="(versionLoadResult$ | async)?.done || errorMessage; else progress"> <mat-spinner mode="indeterminate" diameter="80"></mat-spinner>
<section *ngIf="errorMessage"> }
} @else {
@if ((versionLoadResult$ | async)?.done || errorMessage) {
<section [class.hidden]="!errorMessage">
<div class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div> <div class="mat-headline-6 vc-result-message error" [innerHTML]="errorMessage"></div>
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<button mat-button color="primary" <button mat-button color="primary"
@ -70,13 +73,12 @@
</button> </button>
</div> </div>
</section> </section>
</section> } @else {
<ng-template #progress>
<section class="flex flex-col items-center justify-center"> <section class="flex flex-col items-center justify-center">
<div class="mat-headline-6 vc-result-message progress"> <div class="mat-headline-6 vc-result-message progress">
<span translate>version-control.restoring-entity-version</span> <span translate>version-control.restoring-entity-version</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar> <mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div> </div>
</section> </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;"> <form [formGroup]="confirmFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="(isLoading$ | async)"> <fieldset [disabled]="(isLoading$ | async)">
<div style="user-select: none;" [innerHTML]="removeOtherEntitiesConfirmText"></div> <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"/> <input matInput formControlName="verification"/>
</mat-form-field> </mat-form-field>
</fieldset> </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" <mat-option *ngFor="let column of configuredColumns; let $index = index"
[value]="$index" [value]="$index"
[disabled]="usedCellClickColumns.includes($index)"> [disabled]="usedCellClickColumns.includes($index)">
{{ getCellClickColumnInfo($index, column) }} {{ getCellClickColumnInfo($index, column) | customTranslate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-icon matSuffix <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; color: $enabled-color;
} }
.mat-mdc-progress-bar { .mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($enabled-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$enabled-color}; --mdc-linear-progress-active-indicator-color: #{$enabled-color};
} }
} }
@ -58,6 +59,7 @@ $warning-color: #FAA405;
color: $disabled-color; color: $disabled-color;
} }
.mat-mdc-progress-bar { .mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($disabled-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$disabled-color}; --mdc-linear-progress-active-indicator-color: #{$disabled-color};
} }
} }
@ -66,6 +68,7 @@ $warning-color: #FAA405;
color: $warning-color; color: $warning-color;
} }
.mat-mdc-progress-bar { .mat-mdc-progress-bar {
--mdc-linear-progress-track-color: #{rgba($warning-color, 0.06)};
--mdc-linear-progress-active-indicator-color: #{$warning-color}; --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 { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { DataKey, DatasourceType, widgetType } from "@shared/models/widget.models"; import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { WidgetSubscriptionOptions } from "@core/api/widget-api.models"; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { formattedDataFormDatasourceData } from "@core/utils"; import { formattedDataFormDatasourceData } from '@core/utils';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from '@core/services/utils.service';
import { import {
ApiUsageDataKeysSettings,
apiUsageDefaultSettings, apiUsageDefaultSettings,
ApiUsageWidgetSettings ApiUsageWidgetSettings,
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; getUniqueDataKeys
} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
@Component({ @Component({
selector: 'tb-api-usage-widget', selector: 'tb-api-usage-widget',
@ -80,7 +80,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
type: DatasourceType.entity, type: DatasourceType.entity,
name: '', name: '',
entityAliasId: this.settings.dsEntityAliasId, entityAliasId: this.settings.dsEntityAliasId,
dataKeys: this.getUniqueDataKeys(this.settings.dataKeys) dataKeys: getUniqueDataKeys(this.settings.apiUsageDataKeys)
} }
const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = { const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = {
@ -122,7 +122,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
} }
parseApiUsages() { parseApiUsages() {
this.settings.dataKeys.forEach((key) => { this.settings.apiUsageDataKeys.forEach((key) => {
this.apiUsages.push({ this.apiUsages.push({
label: this.utils.customTranslation(key.label, key.label), label: this.utils.customTranslation(key.label, key.label),
state: key.state, 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() { ngOnDestroy() {
if (this.contentResize$) { if (this.contentResize$) {
this.contentResize$.disconnect(); 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 content: string;
let container: JQuery<HTMLElement>; let container: JQuery<HTMLElement>;
const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); const jQueryContainerElement = $(this.liquidLevelContent.nativeElement);
let value = 'N/A'; let value: number | string = 'N/A';
if (isNumeric(data)) { if (isNumeric(data)) {
value = this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)) value = +this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0);
.toFixed(this.settings.decimals || 0);
} }
this.valueColor.update(value); this.valueColor.update(value);
const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont), const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont),
@ -528,10 +526,9 @@ export class LiquidLevelWidgetComponent implements OnInit {
let volume: number | string; let volume: number | string;
if (this.widgetUnits !== CapacityUnits.percent) { if (this.widgetUnits !== CapacityUnits.percent) {
const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to); const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to);
volume = this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)) volume = +this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0);
.toFixed(this.settings.decimals || 0);
} else { } 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), 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 { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; import { DataKey, Widget, widgetType } from '@shared/models/widget.models';
import { Observable } from "rxjs"; import { Observable } from 'rxjs';
import { BackgroundSettings, BackgroundType } from "@shared/models/widget-settings.models"; import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models';
import { DataKeyType } from "@shared/models/telemetry/telemetry.models"; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { materialColors } from "@shared/models/material.models"; import { materialColors } from '@shared/models/material.models';
export interface ApiUsageSettingsContext { export interface ApiUsageSettingsContext {
aliasController: IAliasController; aliasController: IAliasController;
@ -33,7 +33,7 @@ export interface ApiUsageSettingsContext {
export interface ApiUsageWidgetSettings { export interface ApiUsageWidgetSettings {
dsEntityAliasId: string; dsEntityAliasId: string;
dataKeys: ApiUsageDataKeysSettings[]; apiUsageDataKeys: ApiUsageDataKeysSettings[];
targetDashboardState: string; targetDashboardState: string;
background: BackgroundSettings; background: BackgroundSettings;
padding: string; padding: string;
@ -80,7 +80,7 @@ const generateDataKey = (label: string, status: string, maxLimit: string, curren
export const apiUsageDefaultSettings: ApiUsageWidgetSettings = { export const apiUsageDefaultSettings: ApiUsageWidgetSettings = {
dsEntityAliasId: '', dsEntityAliasId: '',
dataKeys: [ apiUsageDataKeys: [
generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'), generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'),
generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'), generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'),
generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'), generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'),
@ -104,3 +104,15 @@ export const apiUsageDefaultSettings: ApiUsageWidgetSettings = {
padding: '0' 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, ApiUsageDataKeysSettings,
apiUsageDefaultSettings, apiUsageDefaultSettings,
ApiUsageSettingsContext ApiUsageSettingsContext
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; } from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models';
import { deepClone } from "@core/utils"; import { deepClone } from '@core/utils';
import { Observable, of } from "rxjs"; import { Observable, of } from 'rxjs';
import { import {
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigDialogData DataKeyConfigDialogData
} from "@home/components/widget/lib/settings/common/key/data-key-config-dialog.component"; } from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from '@angular/material/dialog';
import { CdkDragDrop } from "@angular/cdk/drag-drop"; import { CdkDragDrop } from '@angular/cdk/drag-drop';
@Component({ @Component({
selector: 'tb-api-usage-widget-settings', selector: 'tb-api-usage-widget-settings',
@ -82,11 +82,11 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
} }
protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { 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 { dataKeysFormArray(): UntypedFormArray {
return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; return this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray;
} }
trackByDataKey(index: number): any { trackByDataKey(index: number): any {
@ -104,7 +104,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
} }
removeDataKey(index: number) { removeDataKey(index: number) {
(this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray).removeAt(index); (this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray).removeAt(index);
} }
addDataKey() { addDataKey() {
@ -115,7 +115,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
maxLimit: null, maxLimit: null,
current: 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()]); const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]);
dataKeysArray.push(dataKeyControl); dataKeysArray.push(dataKeyControl);
} }
@ -131,7 +131,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
return { return {
dsEntityAliasId: settings?.dsEntityAliasId, dsEntityAliasId: settings?.dsEntityAliasId,
dataKeys: settings?.dataKeys, apiUsageDataKeys: settings?.apiUsageDataKeys,
targetDashboardState: settings?.targetDashboardState, targetDashboardState: settings?.targetDashboardState,
background: settings?.background, background: settings?.background,
padding: settings.padding padding: settings.padding
@ -141,7 +141,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.apiUsageWidgetSettingsForm = this.fb.group({ this.apiUsageWidgetSettingsForm = this.fb.group({
dsEntityAliasId: [settings?.dsEntityAliasId], dsEntityAliasId: [settings?.dsEntityAliasId],
dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys), apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys),
targetDashboardState: [settings?.targetDashboardState], targetDashboardState: [settings?.targetDashboardState],
background: [settings?.background, []], background: [settings?.background, []],
padding: [settings.padding, []] 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" <table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"
matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear>
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'"> <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" <mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex) | async" [innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex) | async"
[style]="cellStyle(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 = ''; let title = '';
const header = this.sources[index].header.find(column => column.index.toString() === value); const header = this.sources[index].header.find(column => column.index.toString() === value);
if (value === '0') { if (value === '0') {
title = 'Timestamp'; title = this.translate.instant('widgets.table.timestamp-column-name');
} else if (value === 'actions') { } else if (value === 'actions') {
title = 'Actions'; title = 'Actions';
} else { } 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) { if (this.modelValue) {
const configuredColumns = new Array<CellClickColumnInfo>(); const configuredColumns = new Array<CellClickColumnInfo>();
if (this.modelValue.config?.datasources[0]?.dataKeys?.length) { 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) { if (this.modelValue.config?.alarmSource?.dataKeys?.length) {
configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.alarmSource.dataKeys)); 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'"> [class.!hidden]="isEdit || dashboardScope !== 'tenant'">
{{'dashboard.export' | translate }} {{'dashboard.export' | translate }}
</button> </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" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')" (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 { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
Dashboard, Dashboard, DashboardInfo,
getDashboardAssignedCustomersText, getDashboardAssignedCustomersText,
isCurrentPublicDashboardCustomer, isCurrentPublicDashboardCustomer,
isPublicDashboard isPublicDashboard
@ -31,13 +31,14 @@ import { DashboardService } from '@core/http/dashboard.service';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { PageLink } from "@shared/models/page/page-link";
@Component({ @Component({
selector: 'tb-dashboard-form', selector: 'tb-dashboard-form',
templateUrl: './dashboard-form.component.html', templateUrl: './dashboard-form.component.html',
styleUrls: ['./dashboard-form.component.scss'] 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'; dashboardScope: 'tenant' | 'customer' | 'customer_user' | 'edge';
customerId: string; customerId: string;
@ -117,7 +118,7 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
onPublicLinkCopied($event) { onPublicLinkCopied($event) {
this.store.dispatch(new ActionNotificationShow( this.store.dispatch(new ActionNotificationShow(
{ {
message: this.translate.instant('dashboard.public-link-copied-message'), message: this.translate.instant('dashboard.public-link-copied-message'),
type: 'success', type: 'success',
duration: 750, 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 { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component'; 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 { 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 { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { ImportDashboardFileDialogComponent } from "@home/pages/dashboard/import-dashboard-file-dialog.component";
@NgModule({ @NgModule({
declarations: [ declarations: [
DashboardFormComponent, DashboardFormComponent,
DashboardTabsComponent, DashboardTabsComponent,
ManageDashboardCustomersDialogComponent, ManageDashboardCustomersDialogComponent,
MakeDashboardPublicDialogComponent MakeDashboardPublicDialogComponent,
ImportDashboardFileDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -40,4 +46,5 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
DashboardRoutingModule 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, EntityAliasesDialogComponent,
EntityAliasesDialogData EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component'; } 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() @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>, constructor(private store: Store<AppState>,
private dashboardService: DashboardService, private dashboardService: DashboardService,
@ -375,7 +380,7 @@ export class DashboardsTableConfigResolver {
return actions; return actions;
} }
openDashboard($event: Event, dashboard: DashboardInfo) { openDashboard($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -422,13 +427,27 @@ export class DashboardsTableConfigResolver {
)); ));
} }
exportDashboard($event: Event, dashboard: DashboardInfo) { exportDashboard($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
this.importExport.exportDashboard(dashboard.id.id); 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) { addDashboardsToCustomer($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -449,7 +468,7 @@ export class DashboardsTableConfigResolver {
}); });
} }
makePublic($event: Event, dashboard: DashboardInfo) { makePublic($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -470,7 +489,7 @@ export class DashboardsTableConfigResolver {
); );
} }
makePrivate($event: Event, dashboard: DashboardInfo) { makePrivate($event: Event, dashboard: Dashboard) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -492,7 +511,7 @@ export class DashboardsTableConfigResolver {
); );
} }
manageAssignedCustomers($event: Event, dashboard: DashboardInfo) { manageAssignedCustomers($event: Event, dashboard: Dashboard) {
const assignedCustomersIds = dashboard.assignedCustomers ? const assignedCustomersIds = dashboard.assignedCustomers ?
dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : []; dashboard.assignedCustomers.map(customerInfo => customerInfo.customerId.id) : [];
this.showManageAssignedCustomersDialog($event, [dashboard.id.id], 'manage', assignedCustomersIds); 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) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -579,7 +598,7 @@ export class DashboardsTableConfigResolver {
); );
} }
onDashboardAction(action: EntityAction<DashboardInfo>): boolean { onDashboardAction(action: EntityAction<Dashboard>): boolean {
switch (action.action) { switch (action.action) {
case 'open': case 'open':
this.openDashboard(action.event, action.entity); this.openDashboard(action.event, action.entity);
@ -587,6 +606,9 @@ export class DashboardsTableConfigResolver {
case 'export': case 'export':
this.exportDashboard(action.event, action.entity); this.exportDashboard(action.event, action.entity);
return true; return true;
case 'import':
this.importDashboardFile(action.event, action.entity);
return true;
case 'makePublic': case 'makePublic':
this.makePublic(action.event, action.entity); this.makePublic(action.event, action.entity);
return true; 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="notification-content mini" [style.background-color]="preview.processedTemplates.MICROSOFT_TEAMS?.themeColor">
<div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div> <div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }} {{ 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> </div>
</section> </section>
</div> </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 { TimeService } from '@core/services/time.service';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { isDefined } from '@core/utils';
@Component({ @Component({
selector: 'tb-datapoints-limit', selector: 'tb-datapoints-limit',
@ -69,7 +70,11 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
@Input() @Input()
disabled: boolean; disabled: boolean;
private propagateChange = (v: any) => { }; private propagateChangeValue: any;
private propagateChange = (v: any) => {
this.propagateChangeValue = v;
};
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -79,6 +84,9 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
if (isDefined(this.propagateChangeValue)) {
this.propagateChange(this.propagateChangeValue);
}
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
@ -115,19 +123,20 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator
} }
} }
private checkLimit(limit?: number): number { writeValue(value: number | null): void {
if (!limit || limit < this.minDatapointsLimit()) { this.modelValue = value;
return this.minDatapointsLimit(); 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()) { } else if (limit > this.maxDatapointsLimit()) {
return this.maxDatapointsLimit(); limit = this.maxDatapointsLimit();
} }
return limit;
}
writeValue(value: number | null): void { this.updateView(limit);
this.modelValue = this.checkLimit(value);
this.datapointsLimitFormGroup.patchValue( 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. 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> <mat-label>{{ 'version-control.branch' | translate }}</mat-label>
<input matInput type="text" placeholder="{{emptyPlaceholder || ((loading ? 'common.loading' : 'version-control.select-branch') | translate)}}" <input matInput type="text" placeholder="{{emptyPlaceholder || ((loading ? 'common.loading' : 'version-control.select-branch') | translate)}}"
#branchInput #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 { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr } from '@core/utils';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { SubscriptSizing } from '@angular/material/form-field'; import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field';
@Component({ @Component({
selector: 'tb-branch-autocomplete', selector: 'tb-branch-autocomplete',
@ -57,6 +57,9 @@ export class BranchAutocompleteComponent implements ControlValueAccessor, OnInit
@Input() @Input()
subscriptSizing: SubscriptSizing = 'fixed'; subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
private requiredValue: boolean; private requiredValue: boolean;
get required(): 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, export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => { historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => {
const noneAggregation = hasAggregation && timewindow.aggregation?.type === AggregationType.NONE;
if (timewindow.selectedTab === TimewindowType.REALTIME) { if (timewindow.selectedTab === TimewindowType.REALTIME) {
if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) { if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
delete timewindow.realtime.timewindowMs; delete timewindow.realtime.timewindowMs;
@ -1138,7 +1139,7 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly:
delete timewindow.history?.quickInterval; delete timewindow.history?.quickInterval;
delete timewindow.history?.interval; delete timewindow.history?.interval;
if (!hasAggregation) { if (!hasAggregation || noneAggregation) {
delete timewindow.realtime.interval; delete timewindow.realtime.interval;
} }
} else { } else {
@ -1162,13 +1163,15 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly:
delete timewindow.realtime?.quickInterval; delete timewindow.realtime?.quickInterval;
delete timewindow.realtime?.interval; delete timewindow.realtime?.interval;
if (!hasAggregation) { if (!hasAggregation || noneAggregation) {
delete timewindow.history.interval; delete timewindow.history.interval;
} }
} }
if (!hasAggregation) { if (!hasAggregation) {
delete timewindow.aggregation; delete timewindow.aggregation;
} else if (!noneAggregation) {
delete timewindow.aggregation.limit;
} }
if (historyOnly) { 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 { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.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 { import {
additionalMapDataSourcesToDatasources, additionalMapDataSourcesToDatasources,
BaseMapSettings, BaseMapSettings,
CirclesDataLayerSettings,
MapDataLayerSettings, MapDataLayerSettings,
MapDataLayerType, MapDataLayerType,
MapDataSourceSettings, MapDataSourceSettings,
mapDataSourceSettingsToDatasource, mapDataSourceSettingsToDatasource,
MapType, MapType
MarkersDataLayerSettings,
PolygonsDataLayerSettings
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
@ -126,7 +123,7 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
datasources.push(...getMapDataLayersDatasources(settings.circles)); datasources.push(...getMapDataLayersDatasources(settings.circles));
} }
if (settings.additionalDataSources?.length) { if (settings.additionalDataSources?.length) {
datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources)); datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));
} }
return datasources; return datasources;
}, },
@ -137,13 +134,13 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
} else { } else {
const datasources: Datasource[] = []; const datasources: Datasource[] = [];
if (settings.markers?.length) { if (settings.markers?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.markers, 'markers')); datasources.push(...getMapDataLayersDatasources(settings.markers, true, 'markers'));
} }
if (settings.polygons?.length) { if (settings.polygons?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.polygons, 'polygons')); datasources.push(...getMapDataLayersDatasources(settings.polygons, true, 'polygons'));
} }
if (settings.circles?.length) { if (settings.circles?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.circles, 'circles')); datasources.push(...getMapDataLayersDatasources(settings.circles, true, 'circles'));
} }
if (settings.additionalDataSources?.length) { if (settings.additionalDataSources?.length) {
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); 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[] = []; const datasources: Datasource[] = [];
settings.forEach((dsSettings) => { settings.forEach((dsSettings) => {
datasources.push(mapDataSourceSettingsToDatasource(dsSettings)); const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings, null, includeDataKeys, dataLayerType);
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);
datasources.push(datasource); datasources.push(datasource);
if ((dsSettings).additionalDataSources?.length) { if (dsSettings.additionalDataSources?.length) {
(dsSettings).additionalDataSources.forEach((ds) => { dsSettings.additionalDataSources.forEach((ds) => {
const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds); const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds);
additionalDatasource.dataKeys.push(...dataKeys); if (includeDataKeys) {
additionalDatasource.dataKeys.push(...datasource.dataKeys);
}
datasources.push(additionalDatasource); datasources.push(additionalDatasource);
}); });
} }
}); });
return datasources; 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'; } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { import {
deepClone,
guid, guid,
hashCode, hashCode,
isDefinedAndNotNull, isDefinedAndNotNull,
@ -61,19 +62,44 @@ export interface TbMapDatasource extends Datasource {
mapDataIds: string[]; 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 { return {
type: settings.dsType, type: settings.dsType,
name: settings.dsLabel, name: settings.dsLabel,
deviceId: settings.dsDeviceId, deviceId: settings.dsDeviceId,
entityAliasId: settings.dsEntityAliasId, entityAliasId: settings.dsEntityAliasId,
filterId: settings.dsFilterId, filterId: settings.dsFilterId,
dataKeys: [], dataKeys: dataKeys,
latestDataKeys: [], latestDataKeys: [],
mapDataIds: [id] 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 { export enum DataLayerPatternType {
pattern = 'pattern', pattern = 'pattern',
@ -658,10 +684,11 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings {
dataKeys: DataKey[]; dataKeys: DataKey[];
} }
export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => { export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[],
includeDataKeys = true): TbMapDatasource[] => {
return additionalMapDataSources.map(addDs => { return additionalMapDataSources.map(addDs => {
const res = mapDataSourceSettingsToDatasource(addDs); const res = mapDataSourceSettingsToDatasource(addDs);
res.dataKeys = addDs.dataKeys; res.dataKeys = includeDataKeys ? addDs.dataKeys : [];
return res; 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 { EntityAliases } from '@shared/models/alias.models';
import { Filters } from '@shared/models/query/query.models'; import { Filters } from '@shared/models/query/query.models';
import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition'; 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> { export interface WidgetModelDefinition<T = any> {
testWidget(widget: Widget): boolean; testWidget(widget: Widget): boolean;
@ -29,7 +30,8 @@ export interface WidgetModelDefinition<T = any> {
} }
const widgetModelRegistry: WidgetModelDefinition[] = [ const widgetModelRegistry: WidgetModelDefinition[] = [
MapModelDefinition MapModelDefinition,
ApiUsageModelDefinition
]; ];
export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => { 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-order": "Dashboard order in mobile application",
"mobile-hide": "Hide dashboard in mobile application", "mobile-hide": "Hide dashboard in mobile application",
"update-image": "Update dashboard image", "update-image": "Update dashboard image",
"update-new-version": "Upload new version",
"upload-file-to-update": "Upload file to update",
"take-screenshot": "Take screenshot", "take-screenshot": "Take screenshot",
"select-widget-title": "Select widget", "select-widget-title": "Select widget",
"select-widget-value": "{{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", "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", "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons",
"display-timestamp": "Timestamp", "display-timestamp": "Timestamp",
"timestamp-column-name":"Timestamp",
"display-pagination": "Display pagination", "display-pagination": "Display pagination",
"default-page-size": "Default page size", "default-page-size": "Default page size",
"page-step-settings": "Page step settings", "page-step-settings": "Page step settings",
@ -9601,7 +9604,8 @@
"add-key": "Add key", "add-key": "Add key",
"no-key": "No key", "no-key": "No key",
"delete-key": "Delete 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": { "color": {

Loading…
Cancel
Save