Browse Source

[4421] resolved conflicts

pull/11684/head
mpetrov 2 years ago
parent
commit
0dbbb10cc3
  1. 2
      application/src/main/data/json/system/widget_types/rpc_remote_shell.json
  2. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  3. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  4. 22
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
  5. 2
      ui-ngx/src/app/core/auth/auth.service.ts
  6. 6
      ui-ngx/src/app/core/services/utils.service.ts
  7. 57
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts
  8. 37
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts
  9. 8
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html
  10. 67
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts
  11. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html
  12. 11
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss
  13. 90
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts
  14. 96
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  15. 12
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  16. 9
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  17. 27
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  18. 15
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  19. 21
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  20. 17
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts
  21. 17
      ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts
  22. 17
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  23. 28
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html
  24. 80
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  25. 12
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  26. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract.ts
  27. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts
  28. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html
  29. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts
  30. 48
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts
  31. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  32. 56
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  33. 80
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  34. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.html
  35. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  36. 33
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  37. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts
  38. 17
      ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts
  39. 4
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  40. 20
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts
  41. 28
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  42. 17
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  43. 17
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  44. 4
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  45. 30
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  46. 7
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts
  47. 13
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts
  48. 21
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts
  49. 18
      ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts
  50. 5
      ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html
  51. 5
      ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts
  52. 22
      ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts
  53. 8
      ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html
  54. 12
      ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts
  55. 7
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts
  56. 16
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  57. 11
      ui-ngx/src/app/shared/components/contact.component.html
  58. 2
      ui-ngx/src/app/shared/components/country-autocomplete.component.ts
  59. 4
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html
  60. 5
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts
  61. 9
      ui-ngx/src/app/shared/components/grid/scroll-grid.component.ts
  62. 17
      ui-ngx/src/app/shared/components/image/image-gallery.component.ts
  63. 9
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  64. 11
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  65. 42
      ui-ngx/src/app/shared/models/alarm.models.ts
  66. 2
      ui-ngx/src/assets/dashboard/sys_admin_home_page.json
  67. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  68. 2
      ui-ngx/src/form.scss
  69. 3
      ui-ngx/src/styles.scss

2
application/src/main/data/json/system/widget_types/rpc_remote_shell.json

File diff suppressed because one or more lines are too long

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java

@ -35,7 +35,7 @@ import java.util.List;
type = ComponentType.EXTERNAL,
name = "rest api call",
configClazz = TbRestApiCallNodeConfiguration.class,
version = 2,
version = 3,
nodeDescription = "Invoke REST API calls to external REST server",
nodeDetails = "Will invoke REST API call <code>GET | POST | PUT | DELETE</code> to external REST server. " +
"Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." +
@ -52,6 +52,7 @@ import java.util.List;
public class TbRestApiCallNode extends TbAbstractExternalNode {
static final String PARSE_TO_PLAIN_TEXT = "parseToPlainText";
static final String MAX_IN_MEMORY_BUFFER_SIZE_IN_KB = "maxInMemoryBufferSizeInKb";
static final String TRIM_DOUBLE_QUOTES = "trimDoubleQuotes";
protected TbHttpClient httpClient;
@ -92,6 +93,11 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
hasChanges = true;
((ObjectNode) oldConfiguration).remove(List.of("useRedisQueueForMsgPersistence", "trimQueue", "maxQueueSize"));
}
case 2:
if (!oldConfiguration.has(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB)) {
hasChanges = true;
((ObjectNode) oldConfiguration).put(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB, 256);
}
break;
default:
break;

50
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

22
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java

@ -233,7 +233,8 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}"),
"\"credentials\": {\"type\": \"anonymous\"}," +
"\"maxInMemoryBufferSizeInKb\": 256}"),
// config for version 2 with upgrade from version 1
Arguments.of(1,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
@ -249,7 +250,24 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}")
"\"credentials\": {\"type\": \"anonymous\"}," +
"\"maxInMemoryBufferSizeInKb\": 256}"),
// config for version 3 with upgrade from version 2
Arguments.of(2,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
"\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," +
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}",
true,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
"\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," +
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}," +
"\"maxInMemoryBufferSizeInKb\": 256}")
);
}

2
ui-ngx/src/app/core/auth/auth.service.ts

@ -350,7 +350,7 @@ export class AuthService {
)
);
} else if (loginError) {
this.showLoginErrorDialog(loginError);
Promise.resolve().then(() => this.showLoginErrorDialog(loginError));
this.utils.updateQueryParam('loginError', null);
return throwError(Error());
}

6
ui-ngx/src/app/core/services/utils.service.ts

@ -41,7 +41,11 @@ import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields, alarmSeverityTranslations, alarmStatusTranslations } from '@shared/models/alarm.models';
import {
alarmFields,
alarmSeverityTranslations,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults';

57
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts

@ -44,7 +44,7 @@ import { AlarmService } from '@core/http/alarm.service';
import { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service';
import { AlarmAssigneeOption } from '@shared/models/alarm.models';
import { AlarmAssigneeOption, getUserDisplayName, getUserInitials } from '@shared/models/alarm.models';
export const ALARM_ASSIGNEE_PANEL_DATA = new InjectionToken<any>('AlarmAssigneePanelData');
@ -110,9 +110,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges
.pipe(
debounceTime(150),
map(value => {
return value ? (typeof value === 'string' ? value : '') : ''
}),
map(value => value ? (typeof value === 'string' ? value : '') : ''),
distinctUntilChanged(),
switchMap(name => this.fetchUsers(name)),
share(),
@ -123,7 +121,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
ngAfterViewInit() {
setTimeout(() => {
this.userInput.nativeElement.focus();
}, 0)
}, 0);
}
ngOnDestroy() {
@ -149,7 +147,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
this.alarmService.assignAlarm(this.alarmId, user.id.id, {ignoreLoading: true}).subscribe(
() => {
this.reassigned = true;
this.overlayRef.dispose()
this.overlayRef.dispose();
});
}
@ -157,7 +155,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
this.alarmService.unassignAlarm(this.alarmId, {ignoreLoading: true}).subscribe(
() => {
this.reassigned = true;
this.overlayRef.dispose()
this.overlayRef.dispose();
});
}
@ -170,9 +168,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
return this.userService.getUsersForAssign(this.alarmId, pageLink, {ignoreLoading: true})
.pipe(
catchError(() => of(emptyPageData<UserEmailInfo>())),
map(pageData => {
return pageData.data;
})
map(pageData => pageData.data)
);
}
@ -191,42 +187,11 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
}, 0);
}
getUserDisplayName(entity: User) {
let displayName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
displayName += entity.firstName;
}
if (entity.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.lastName;
}
} else {
displayName = entity.email;
}
return displayName;
}
getUserInitials(entity: User): string {
let initials = '';
if (entity.firstName && entity.firstName.length ||
entity.lastName && entity.lastName.length) {
if (entity.firstName) {
initials += entity.firstName.charAt(0);
}
if (entity.lastName) {
initials += entity.lastName.charAt(0);
}
} else {
initials += entity.email.charAt(0);
}
return initials.toUpperCase();
getUserInitials(entity: UserEmailInfo): string {
return getUserInitials(entity);
}
getFullName(entity: User): string {
getFullName(entity: UserEmailInfo): string {
let fullName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
@ -243,8 +208,8 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
return fullName;
}
getAvatarBgColor(entity: User) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
getAvatarBgColor(entity: UserEmailInfo) {
return this.utilsService.stringToHslColor(getUserDisplayName(entity), 40, 60);
}
}

37
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts

@ -36,7 +36,7 @@ import { emptyPageData } from '@shared/models/page/page-data';
import { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service';
import { AlarmAssigneeOption } from '@shared/models/alarm.models';
import { AlarmAssigneeOption, getUserDisplayName, getUserInitials } from '@shared/models/alarm.models';
export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken<any>('AlarmAssigneeSelectPanelData');
@ -165,39 +165,8 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit
}, 0);
}
getUserDisplayName(entity: UserEmailInfo) {
let displayName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
displayName += entity.firstName;
}
if (entity.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.lastName;
}
} else {
displayName = entity.email;
}
return displayName;
}
getUserInitials(entity: UserEmailInfo): string {
let initials = '';
if (entity.firstName && entity.firstName.length ||
entity.lastName && entity.lastName.length) {
if (entity.firstName) {
initials += entity.firstName.charAt(0);
}
if (entity.lastName) {
initials += entity.lastName.charAt(0);
}
} else {
initials += entity.email.charAt(0);
}
return initials.toUpperCase();
return getUserInitials(entity);
}
getFullName(entity: UserEmailInfo): string {
@ -218,7 +187,7 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit
}
getAvatarBgColor(entity: UserEmailInfo) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
return this.utilsService.stringToHslColor(getUserDisplayName(entity), 40, 60);
}
}

8
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html

@ -20,10 +20,12 @@
subscriptSizing="dynamic">
<mat-label translate>alarm.assignee</mat-label>
<input matInput readonly [value]="getAssignee()">
<span *ngIf="alarm?.assigneeId" matPrefix class="user-avatar"
[style.backgroundColor]="getAvatarBgColor(alarm.assignee)">
<span *ngIf="userAssigned; else unassigned" matPrefix class="user-avatar"
[style.background-color]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }}
</span>
<mat-icon *ngIf="!alarm?.assigneeId" matPrefix class="unassigned-icon">account_circle</mat-icon>
<ng-template #unassigned>
<mat-icon matPrefix class="material-icons unassigned-icon">{{ alarm?.assigneeId ? 'no_accounts' : 'account_circle' }}</mat-icon>
</ng-template>
<mat-icon matSuffix>arrow_drop_down</mat-icon>
</mat-form-field>

67
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts

@ -16,7 +16,7 @@
import { Component, EventEmitter, Injector, Input, Output, StaticProvider, ViewContainerRef } from '@angular/core';
import { UtilsService } from '@core/services/utils.service';
import { AlarmAssignee, AlarmInfo } from '@shared/models/alarm.models';
import { AlarmAssignee, AlarmInfo, getUserDisplayName, getUserInitials } from '@shared/models/alarm.models';
import {
ALARM_ASSIGNEE_PANEL_DATA,
AlarmAssigneePanelComponent,
@ -25,6 +25,7 @@ import {
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core';
import { isNotEmptyStr } from '@core/utils';
@Component({
selector: 'tb-alarm-assignee',
@ -41,6 +42,8 @@ export class AlarmAssigneeComponent {
@Output()
alarmReassigned = new EventEmitter<boolean>();
userAssigned: boolean;
constructor(private utilsService: UtilsService,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
@ -49,68 +52,22 @@ export class AlarmAssigneeComponent {
getAssignee() {
if (this.alarm) {
if (this.alarm.assignee) {
return this.getUserDisplayName(this.alarm.assignee);
this.userAssigned = this.alarm.assigneeId && ((isNotEmptyStr(this.alarm.assignee?.firstName) ||
isNotEmptyStr(this.alarm.assignee?.lastName)) || isNotEmptyStr(this.alarm.assignee?.email));
if (this.userAssigned) {
return getUserDisplayName(this.alarm.assignee);
} else {
return this.translateService.instant('alarm.unassigned');
}
}
}
getUserDisplayName(entity: AlarmAssignee) {
let displayName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
displayName += entity.firstName;
}
if (entity.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.lastName;
}
} else {
displayName = entity.email;
}
return displayName;
}
getUserInitials(entity: AlarmAssignee): string {
let initials = '';
if (entity.firstName && entity.firstName.length ||
entity.lastName && entity.lastName.length) {
if (entity.firstName) {
initials += entity.firstName.charAt(0);
}
if (entity.lastName) {
initials += entity.lastName.charAt(0);
return this.translateService.instant(this.alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned');
}
} else {
initials += entity.email.charAt(0);
}
return initials.toUpperCase();
}
getFullName(entity: AlarmAssignee): string {
let fullName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
fullName += entity.firstName;
}
if (entity.lastName) {
if (fullName.length > 0) {
fullName += ' ';
}
fullName += entity.lastName;
}
}
return fullName;
getUserInitials(alarmAssignee: AlarmAssignee): string {
return getUserInitials(alarmAssignee);
}
getAvatarBgColor(entity: AlarmAssignee) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
return this.utilsService.stringToHslColor(getUserDisplayName(entity), 40, 60);
}
openAlarmAssigneePanel($event: Event, alarm: AlarmInfo) {

6
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html

@ -69,10 +69,14 @@
*ngIf="!displayDataElement.edit; else commentEditing"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
(mouseleave)="onCommentMouseLeave(i)">
<div class="user-avatar" fxLayout="row" fxLayoutAlign="center center" fxFlexAlign="start" fxHide.xs
<div *ngIf="displayDataElement.userExists; else userDeleted" class="user-avatar"
fxLayout="row" fxLayoutAlign="center center" fxFlexAlign="start" fxHide.xs
[style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
<ng-template #userDeleted>
<mat-icon class="mat-icon tb-mat-28" fxHide.xs>no_accounts</mat-icon>
</ng-template>
<div fxFlex fxLayout="column" fxLayoutGap="5px">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<span class="user-name">{{ displayDataElement.displayName }}</span>

11
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss

@ -86,7 +86,7 @@ $border: 1px solid mat.get-color-from-palette($tb-primary);
background-color: $primary-color;
border: $border;
border-bottom: none;
border-radius: 8px 8px 0px 0px;
border-radius: 8px 8px 0 0;
&.activity-only {
border: none;
@ -94,7 +94,7 @@ $border: 1px solid mat.get-color-from-palette($tb-primary);
&.asc {
border-bottom: $border;
box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);
box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08);
&.activity-only {
border: none;
@ -116,7 +116,7 @@ $border: 1px solid mat.get-color-from-palette($tb-primary);
.comments-container {
border: $border;
border-top: none;
border-radius: 0px 0px 8px 8px;
border-radius: 0 0 8px 8px;
&.activity-only {
border: none;
@ -134,6 +134,11 @@ $border: 1px solid mat.get-color-from-palette($tb-primary);
&:hover {
background-color: $primary-color;
}
.mat-icon {
align-self: start;
color: rgba(0, 0, 0, 0.38);
}
}
.user-avatar {

90
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts

@ -21,31 +21,33 @@ import { TranslateService } from '@ngx-translate/core';
import { AlarmCommentService } from '@core/http/alarm-comment.service';
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service';
import { AuthUser, User } from '@shared/models/user.model';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { map } from 'rxjs/operators';
import { AlarmComment, AlarmCommentInfo, AlarmCommentType } from '@shared/models/alarm.models';
import { AlarmComment, AlarmCommentType, getUserDisplayName } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import { DatePipe } from '@angular/common';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { isNotEmptyStr } from '@core/utils';
interface AlarmCommentsDisplayData {
commentId?: string,
displayName?: string,
createdTime: string,
createdDateAgo?: string,
edit?: boolean,
isEdited?: boolean,
commentId?: string;
displayName?: string;
createdTime: string;
createdDateAgo?: string;
edit?: boolean;
isEdited?: boolean;
editedTime?: string;
editedDateAgo?: string,
showActions?: boolean,
commentText?: string,
isSystemComment?: boolean,
avatarBgColor?: string
editedDateAgo?: string;
showActions?: boolean;
commentText?: string;
isSystemComment?: boolean;
avatarBgColor?: string;
userExists?: boolean;
}
@Component({
@ -58,7 +60,7 @@ export class AlarmCommentComponent implements OnInit {
alarmId: string;
@Input()
alarmActivityOnly: boolean = false;
alarmActivityOnly = false;
authUser: AuthUser;
@ -73,11 +75,11 @@ export class AlarmCommentComponent implements OnInit {
direction: Direction.DESC
};
editMode: boolean = false;
editMode = false;
userDisplayName$ = this.store.pipe(
select(selectUserDetails),
map((user) => this.getUserDisplayName(user))
map((user) => getUserDisplayName(user))
);
currentUserDisplayName: string;
@ -115,15 +117,18 @@ export class AlarmCommentComponent implements OnInit {
(pagedData) => {
this.alarmComments = pagedData.data;
this.displayData.length = 0;
for (let alarmComment of pagedData.data) {
let displayDataElement = {} as AlarmCommentsDisplayData;
for (const alarmComment of pagedData.data) {
const displayDataElement = {} as AlarmCommentsDisplayData;
displayDataElement.createdTime = this.datePipe.transform(alarmComment.createdTime, 'yyyy-MM-dd HH:mm:ss');
displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime);
displayDataElement.commentText = alarmComment.comment.text;
displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM;
if (alarmComment.type === AlarmCommentType.OTHER) {
displayDataElement.commentId = alarmComment.id.id;
displayDataElement.displayName = this.getUserDisplayName(alarmComment);
displayDataElement.userExists = isNotEmptyStr(alarmComment.firstName) || isNotEmptyStr(alarmComment.lastName) ||
isNotEmptyStr(alarmComment.email);
displayDataElement.displayName = displayDataElement.userExists ? getUserDisplayName(alarmComment) :
this.translate.instant('alarm.user-deleted');
displayDataElement.edit = false;
displayDataElement.isEdited = alarmComment.comment.edited;
displayDataElement.editedTime = this.datePipe.transform(alarmComment.comment.editedOn, 'yyyy-MM-dd HH:mm:ss');
@ -136,17 +141,17 @@ export class AlarmCommentComponent implements OnInit {
this.displayData.push(displayDataElement);
}
}
)
);
}
changeSortDirection() {
let currentDirection = this.alarmCommentSortOrder.direction;
const currentDirection = this.alarmCommentSortOrder.direction;
this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC;
this.loadAlarmComments();
}
exportAlarmActivity() {
let fileName = this.translate.instant('alarm.alarm') + '_' + this.translate.instant('alarm-activity.activity');
const fileName = this.translate.instant('alarm.alarm') + '_' + this.translate.instant('alarm-activity.activity');
this.importExportService.exportCsv(this.getDataForExport(), fileName.toLowerCase());
}
@ -162,7 +167,7 @@ export class AlarmCommentComponent implements OnInit {
comment: {
text: commentInputValue
}
}
};
this.doSave(comment);
this.clearCommentInput();
}
@ -185,7 +190,7 @@ export class AlarmCommentComponent implements OnInit {
() => {
this.loadAlarmComments();
}
)
);
}
editComment(commentId: string): void {
@ -217,18 +222,18 @@ export class AlarmCommentComponent implements OnInit {
.subscribe(() => {
this.loadAlarmComments();
}
)
);
}
}
)
);
}
getSortDirectionIcon() {
return this.alarmCommentSortOrder.direction === Direction.DESC ? 'mdi:sort-descending' : 'mdi:sort-ascending'
return this.alarmCommentSortOrder.direction === Direction.DESC ? 'mdi:sort-descending' : 'mdi:sort-ascending';
}
getSortDirectionTooltipText() {
let text = this.alarmCommentSortOrder.direction === Direction.DESC ? 'alarm-activity.newest-first' :
const text = this.alarmCommentSortOrder.direction === Direction.DESC ? 'alarm-activity.newest-first' :
'alarm-activity.oldest-first';
return this.translate.instant(text);
}
@ -264,25 +269,6 @@ export class AlarmCommentComponent implements OnInit {
return this.utilsService.stringToHslColor(userDisplayName, 40, 60);
}
private getUserDisplayName(alarmCommentInfo: AlarmCommentInfo | User): string {
let name = '';
if ((alarmCommentInfo.firstName && alarmCommentInfo.firstName.length > 0) ||
(alarmCommentInfo.lastName && alarmCommentInfo.lastName.length > 0)) {
if (alarmCommentInfo.firstName) {
name += alarmCommentInfo.firstName;
}
if (alarmCommentInfo.lastName) {
if (name.length > 0) {
name += ' ';
}
name += alarmCommentInfo.lastName;
}
} else {
name = alarmCommentInfo.email;
}
return name;
}
getAlarmCommentFormControl(): AbstractControl {
return this.alarmCommentFormGroup.get('alarmComment');
}
@ -308,16 +294,16 @@ export class AlarmCommentComponent implements OnInit {
}
private getDataForExport() {
let dataToExport = [];
for (let row of this.displayData) {
let exportRow = {
const dataToExport = [];
for (const row of this.displayData) {
const exportRow = {
[this.translate.instant('alarm-activity.author')]: row.isSystemComment ?
this.translate.instant('alarm-activity.system') : row.displayName,
[this.translate.instant('alarm-activity.created-date')]: row.createdTime,
[this.translate.instant('alarm-activity.edited-date')]: row.editedTime,
[this.translate.instant('alarm-activity.text')]: row.commentText
}
dataToExport.push(exportRow)
};
dataToExport.push(exportRow);
}
return dataToExport;
}

96
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -31,14 +31,16 @@ import { forkJoin, Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import {
AlarmAssignee,
AlarmInfo,
AlarmQueryV2,
AlarmSearchStatus,
alarmSeverityColors,
alarmSeverityTranslations,
AlarmsMode,
AlarmStatus,
alarmStatusTranslations
alarmStatusTranslations,
getUserDisplayName,
getUserInitials
} from '@app/shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service';
import { DialogService } from '@core/services/dialog.service';
@ -60,7 +62,7 @@ import {
AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
import { getEntityDetailsPageURL, isDefinedAndNotNull } from '@core/utils';
import { getEntityDetailsPageURL, isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { UtilsService } from '@core/services/utils.service';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
import { EntityService } from '@core/http/entity.service';
@ -129,22 +131,15 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
})));
this.columns.push(
new EntityTableColumn<AlarmInfo>('assignee', 'alarm.assignee', '240px',
(entity) => {
return this.getAssigneeTemplate(entity)
},
() => ({}),
false,
() => ({}),
(entity) => undefined,
false,
(entity) => this.getAssigneeTemplate(entity), () => ({}), false, () => ({}), () => undefined, false,
{
icon: 'keyboard_arrow_down',
type: CellActionDescriptorType.DEFAULT,
isEnabled: (entity) => true,
isEnabled: () => true,
name: this.translate.instant('alarm.assign'),
onAction: ($event, entity) => this.openAlarmAssigneePanel($event, entity)
})
)
);
this.columns.push(
new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%',
(entity) => this.translate.instant(alarmStatusTranslations.get(entity.status))));
@ -177,7 +172,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
isEnabled: true,
onAction: ($event, entities) => this.deleteAlarms($event, entities)
}
)
);
}
fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> {
@ -216,61 +211,32 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
}
getAssigneeTemplate(entity: AlarmInfo): string {
return `
<span class="assignee-cell">
${isDefinedAndNotNull(entity.assigneeId) ?
`<span class="assigned-container">
<span class="user-avatar" style="background-color: ${this.getAvatarBgColor(entity)}">
${this.getUserInitials(entity)}
</span>
<span class="user-display-name">${this.getUserDisplayName(entity)}</span>
</span>`
:
`<span class="unassigned-container">
<mat-icon class="material-icons unassigned-icon">account_circle</mat-icon>
<span>${this.translate.instant('alarm.unassigned')}</span>
</span>`
}
</span>`
}
const hasAssigneeId = isDefinedAndNotNull(entity.assigneeId);
let templateContent: string;
getUserDisplayName(entity: AlarmInfo) {
let displayName = '';
if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) ||
(entity.assignee.lastName && entity.assignee.lastName.length > 0)) {
if (entity.assignee.firstName) {
displayName += entity.assignee.firstName;
}
if (entity.assignee.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.assignee.lastName;
}
} else {
displayName = entity.assignee.email;
}
return displayName;
}
getUserInitials(entity: AlarmInfo): string {
let initials = '';
if (entity.assignee.firstName && entity.assignee.firstName.length ||
entity.assignee.lastName && entity.assignee.lastName.length) {
if (entity.assignee.firstName) {
initials += entity.assignee.firstName.charAt(0);
}
if (entity.assignee.lastName) {
initials += entity.assignee.lastName.charAt(0);
}
if (hasAssigneeId && ((isNotEmptyStr(entity.assignee?.firstName) || isNotEmptyStr(entity.assignee?.lastName)) ||
isNotEmptyStr(entity.assignee?.email))) {
templateContent = `
<span class="assigned-container">
<span class="user-avatar" style="background-color: ${this.getAvatarBgColor(entity.assignee)}">
${getUserInitials(entity.assignee)}
</span>
<span class="user-display-name">${getUserDisplayName(entity.assignee)}</span>
</span>`;
} else {
initials += entity.assignee.email.charAt(0);
templateContent = `
<span class="unassigned-container">
<mat-icon class="material-icons unassigned-icon">
${hasAssigneeId ? 'no_accounts' : 'account_circle'}
</mat-icon>
<span>${this.translate.instant(hasAssigneeId ? 'alarm.user-deleted' : 'alarm.unassigned')}</span>
</span>`;
}
return initials.toUpperCase();
return `<span class="assignee-cell">${templateContent}</span>`;
}
getAvatarBgColor(entity: AlarmInfo) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
getAvatarBgColor(alarmAssignee: AlarmAssignee) {
return this.utilsService.stringToHslColor(getUserDisplayName(alarmAssignee), 40, 60);
}
openAlarmAssigneePanel($event: Event, entity: AlarmInfo) {
@ -312,7 +278,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.reassigned) {
this.updateData()
this.updateData();
}
});
}

12
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -211,11 +211,13 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
ngOnInit() {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}

9
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -116,17 +116,18 @@
<mat-icon>edit</mat-icon>{{'dashboard.edit-mode' | translate}}</button>
</ng-container>
<ng-container *ngIf="isEdit">
<button fxHide.gt-md mat-icon-button (click)="saveDashboard()"
<button fxHide.gt-md mat-icon-button (click)="saveDashboard()" [disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below">
<mat-icon>done</mat-icon></button>
<button fxHide.lt-lg mat-stroked-button (click)="saveDashboard()">
<button fxHide.lt-lg mat-stroked-button [disabled]="isLoading$ | async" (click)="saveDashboard()">
<mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button>
<button fxHide.gt-md mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'action.cancel' | translate}}"
[disabled]="isLoading$ | async"
matTooltipPosition="below">
<mat-icon>close</mat-icon></button>
<button fxHide.lt-lg mat-button (click)="toggleDashboardEditMode()">
<button fxHide.lt-lg mat-button (click)="toggleDashboardEditMode()" [disabled]="isLoading$ | async">
<mat-icon>close</mat-icon>{{ 'action.cancel' | translate }}</button>
<mat-divider class="tb-toolbar-divider" vertical></mat-divider>
</ng-container>
@ -352,6 +353,7 @@
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="saveDashboard()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below">
<mat-icon>done</mat-icon></button>
@ -360,6 +362,7 @@
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.cancel' | translate}}"
matTooltipPosition="below">
<mat-icon>close</mat-icon></button>

27
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -169,6 +169,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
LayoutType = LayoutType;
private destroyed = false;
private forcePristine = false;
get isDirty(): boolean {
@ -469,7 +471,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
ngAfterViewInit() {
this.dashboardResize$ = new ResizeObserver(() => {
this.updateLayoutSizes();
this.ngZone.run(() => {
this.updateLayoutSizes();
});
});
this.dashboardResize$.observe(this.dashboardContainer.nativeElement);
if (!this.widgetEditMode && !this.readonly && this.dashboardUtils.isEmptyDashboard(this.dashboard)) {
@ -594,6 +598,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
ngOnDestroy(): void {
this.destroyed = true;
this.cleanupDashboardCss();
if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.unregisterToggleLayoutFunction();
@ -1106,16 +1111,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
public openDashboardState(state: string, openRightLayout?: boolean) {
const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
if (layoutsData) {
this.dashboardCtx.state = state;
this.dashboardCtx.aliasController.dashboardStateChanged();
this.isRightLayoutOpened = openRightLayout ? true : false;
this.updateLayouts(layoutsData);
if (!this.destroyed) {
const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
if (layoutsData) {
this.dashboardCtx.state = state;
this.dashboardCtx.aliasController.dashboardStateChanged();
this.isRightLayoutOpened = openRightLayout ? true : false;
this.updateLayouts(layoutsData);
}
setTimeout(() => {
this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened);
});
}
setTimeout(() => {
this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened);
});
}
private updateLayouts(layoutsData?: DashboardLayoutsInfo) {

15
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -248,8 +248,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemCols: 8,
defaultItemRows: 6,
displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50},
draggable: {enabled: this.isEdit && !this.isEditingWidget},
resizable: {
enabled: this.isEdit && !this.isEditingWidget,
delayStart: 50,
stop: (_, itemComponent) => {(itemComponent.item as DashboardWidget).updatePosition(itemComponent.$item.x, itemComponent.$item.y);}
},
draggable: {
enabled: this.isEdit && !this.isEditingWidget,
stop: (_, itemComponent) => {(itemComponent.item as DashboardWidget).updatePosition(itemComponent.$item.x, itemComponent.$item.y);}
},
itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
@ -354,7 +361,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
ngAfterViewInit(): void {
this.gridsterResize$ = new ResizeObserver(() => {
this.onGridsterParentResize();
this.ngZone.run(() => {
this.onGridsterParentResize();
});
});
this.gridsterResize$.observe(this.gridster.el.parentElement);
}

21
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -22,7 +22,7 @@ import {
ComponentFactoryResolver,
ElementRef,
EventEmitter,
Input,
Input, NgZone,
OnChanges,
OnDestroy,
OnInit,
@ -142,7 +142,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
private router: Router,
private componentFactoryResolver: ComponentFactoryResolver,
private elementRef: ElementRef,
private fb: FormBuilder) {
private fb: FormBuilder,
private zone: NgZone) {
super(store);
}
@ -157,11 +158,13 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
});
}
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
@ -432,8 +435,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
}
}
private getTimePageLinkInterval(): {startTime?: number, endTime?: number} {
const interval: {startTime?: number, endTime?: number} = {};
private getTimePageLinkInterval(): {startTime?: number; endTime?: number} {
const interval: {startTime?: number; endTime?: number} = {};
switch (this.timewindow.history.historyType) {
case HistoryWindowType.LAST_INTERVAL:
const currentTime = Date.now();

17
ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts

@ -20,7 +20,7 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
Input,
Input, NgZone,
OnDestroy,
OnInit,
ViewChild
@ -121,7 +121,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
private dialogService: DialogService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
private fb: FormBuilder,
private zone: NgZone) {
super(store);
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC };
@ -133,11 +134,13 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
ngOnInit() {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}

17
ui-ngx/src/app/modules/home/components/vc/entity-versions-table.component.ts

@ -20,7 +20,7 @@ import {
Component,
ElementRef,
EventEmitter,
Input,
Input, NgZone,
OnDestroy,
OnInit,
Output,
@ -141,7 +141,8 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
private cd: ChangeDetectorRef,
private viewContainerRef: ViewContainerRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
private fb: FormBuilder,
private zone: NgZone) {
super(store);
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'timestamp', direction: Direction.DESC };
@ -151,11 +152,13 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
ngOnInit() {
this.componentResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.componentResize$.observe(this.elementRef.nativeElement);
this.isReadOnly = this.adminService.getRepositorySettingsInfo().pipe(map(settings => settings.readOnly));

17
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts

@ -20,7 +20,7 @@ import {
Component,
ElementRef,
forwardRef,
Input,
Input, NgZone,
OnDestroy,
OnInit,
ViewChild
@ -105,7 +105,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
private dialog: MatDialog,
private dialogs: DialogService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
private elementRef: ElementRef,
private zone: NgZone) {
super(store);
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder);
@ -115,11 +116,13 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
ngOnInit(): void {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}

28
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html

@ -88,24 +88,24 @@
<span [innerHTML]="cellContent(alarm, column, row)"></span>
<ng-container *ngIf="column.entityKey.key === 'assignee'">
<span class="assignee-cell" fxLayout="row" fxLayoutAlign="start center">
<span *ngIf="alarm.assigneeId" class="assigned-container">
<span class="user-avatar" [style.backgroundColor]="getAvatarBgColor(alarm)">
{{ getUserInitials(alarm) }}
<span *ngIf="alarm.assigneeId && checkAssigneeHasName(alarm.assignee); else unassigned"
class="assigned-container">
<span class="user-avatar" [style.backgroundColor]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }}
</span>
<span [matTooltip]="getUserDisplayName(alarm)"
matTooltipPosition="above">
{{ getUserDisplayName(alarm) }}
<span [matTooltip]="getUserDisplayName(alarm.assignee)" matTooltipPosition="above">
{{ getUserDisplayName(alarm.assignee) }}
</span>
</span>
<span *ngIf="!alarm.assigneeId" class="unassigned-container">
<mat-icon class="material-icons unassigned-icon">account_circle</mat-icon>
<span matTooltip="{{ 'alarm.unassigned' | translate }}"
matTooltipPosition="above"
style="vertical-align: middle"
translate>
alarm.unassigned
<ng-template #unassigned>
<span class="unassigned-container">
<mat-icon class="material-icons unassigned-icon">{{ alarm.assigneeId ? 'no_accounts' : 'account_circle' }}</mat-icon>
<span matTooltip="{{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }}"
matTooltipPosition="above" style="vertical-align: middle">
{{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }}
</span>
</span>
</span>
</ng-template>
<button *ngIf="allowAssign"
mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'alarm.assign' | translate }}"

80
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -37,7 +37,16 @@ import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/wi
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, hashCode, isDefined, isDefinedAndNotNull, isNumber, isObject, isUndefined } from '@core/utils';
import {
deepClone,
hashCode,
isDefined,
isDefinedAndNotNull,
isNotEmptyStr,
isNumber,
isObject,
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css';
import { sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
@ -83,7 +92,16 @@ import {
DisplayColumnsPanelComponent,
DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component';
import { AlarmDataInfo, alarmFields, AlarmInfo, alarmSeverityColors, AlarmStatus } from '@shared/models/alarm.models';
import {
AlarmAssignee,
AlarmDataInfo,
alarmFields,
AlarmInfo,
alarmSeverityColors,
AlarmStatus,
getUserDisplayName,
getUserInitials
} from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common';
import {
AlarmDetailsDialogComponent,
@ -273,11 +291,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
() => this.pageLink.page = 0
);
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.ngZone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
@ -414,7 +434,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (alarmField && alarmField.time) {
keySettings.columnWidth = '120px';
}
if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) {
if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) {
keySettings.columnWidth = '120px';
}
}
@ -1097,43 +1117,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.rowStyleCache.length = 0;
}
getUserDisplayName(entity: AlarmInfo) {
let displayName = '';
if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) ||
(entity.assignee.lastName && entity.assignee.lastName.length > 0)) {
if (entity.assignee.firstName) {
displayName += entity.assignee.firstName;
}
if (entity.assignee.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.assignee.lastName;
}
} else {
displayName = entity.assignee.email;
}
return displayName;
checkAssigneeHasName(alarmAssignee: AlarmAssignee): boolean {
return (isNotEmptyStr(alarmAssignee?.firstName) || isNotEmptyStr(alarmAssignee?.lastName)) ||
isNotEmptyStr(alarmAssignee?.email);
}
getUserInitials(entity: AlarmInfo): string {
let initials = '';
if (entity.assignee.firstName && entity.assignee.firstName.length ||
entity.assignee.lastName && entity.assignee.lastName.length) {
if (entity.assignee.firstName) {
initials += entity.assignee.firstName.charAt(0);
}
if (entity.assignee.lastName) {
initials += entity.assignee.lastName.charAt(0);
}
} else {
initials += entity.assignee.email.charAt(0);
}
return initials.toUpperCase();
getUserDisplayName(alarmAssignee: AlarmAssignee) {
return getUserDisplayName(alarmAssignee);
}
getUserInitials(alarmAssignee: AlarmAssignee): string {
return getUserInitials(alarmAssignee);
}
getAvatarBgColor(entity: AlarmInfo) {
return this.utils.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
getAvatarBgColor(alarmAssignee: AlarmAssignee) {
return this.utils.stringToHslColor(this.getUserDisplayName(alarmAssignee), 40, 60);
}
openAlarmAssigneePanel($event: Event, entity: AlarmInfo) {

12
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -220,11 +220,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.ctx.updateWidgetParams();
if (this.displayPagination) {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.ngZone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract.ts

@ -74,8 +74,8 @@ export class MqttVersionProcessor extends GatewayConnectorVersionProcessor<MQTTB
getDowngradedVersion(): GatewayConnector<MQTTLegacyBasicConfig> {
const { requestsMapping, mapping, ...restConfig } = this.connector.configurationJson as MQTTBasicConfig_v3_5_2;
const updatedRequestsMapping =
MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>);
const updatedRequestsMapping = requestsMapping
? MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>) : {};
const updatedMapping = MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping);
return {

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts

@ -28,6 +28,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { GatewayConfigValue } from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models';
@Component({
selector: 'tb-gateway-advanced-configuration',
@ -83,8 +84,8 @@ export class GatewayAdvancedConfigurationComponent implements OnDestroy, Control
this.onTouched = fn;
}
writeValue(basicConfig: unknown): void {
this.advancedFormControl.reset(basicConfig, {emitEvent: false});
writeValue(advancedConfig: GatewayConfigValue): void {
this.advancedFormControl.reset(advancedConfig, {emitEvent: false});
}
validate(): ValidationErrors | null {

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html

@ -640,10 +640,11 @@
</mat-form-field>
</section>
<button mat-icon-button (click)="removeCommandControl($index, $event)"
class="tb-box-button"
[disabled]="!basicFormGroup.get('thingsboard.remoteConfiguration').value"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
<mat-icon>delete</mat-icon>
</button>
</div>
<button mat-stroked-button color="primary"

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts

@ -20,7 +20,7 @@ import {
FormGroup,
} from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { DeviceService } from '@core/http/device.service';
@ -40,7 +40,9 @@ import {
GatewayConfigSecurity,
GatewayConfigValue,
GatewayGeneralConfig,
GatewayGRPCConfig,
GatewayLogsConfig,
GatewayStorageConfig,
LocalLogs,
LogAttribute,
LogConfig,
@ -113,7 +115,7 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
this.gatewayConfigGroup.get('basicConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
const advancedControl = this.gatewayConfigGroup.get('advancedConfig');
if (!isEqual(advancedControl.value, value)) {
if (!isEqual(advancedControl.value, value) && this.gatewayConfigGroup.get('mode').value === ConfigurationModes.BASIC) {
advancedControl.patchValue(value, {emitEvent: false});
}
});
@ -121,7 +123,7 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
this.gatewayConfigGroup.get('advancedConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
const basicControl = this.gatewayConfigGroup.get('basicConfig');
if (!isEqual(basicControl.value, value)) {
if (!isEqual(basicControl.value, value) && this.gatewayConfigGroup.get('mode').value === ConfigurationModes.ADVANCED) {
basicControl.patchValue(value, {emitEvent: false});
}
});
@ -320,10 +322,10 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
private updateConfigs(attributes: AttributeData[]): void {
const formValue: GatewayConfigValue = {
thingsboard: null,
grpc: null,
logs: null,
storage: null,
thingsboard: {} as GatewayGeneralConfig,
grpc: {} as GatewayGRPCConfig,
logs: {} as GatewayLogsConfig,
storage: {} as GatewayStorageConfig,
mode: ConfigurationModes.BASIC
};

48
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts

@ -27,32 +27,36 @@ import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-for
export interface GatewayConfigValue {
mode: ConfigurationModes;
thingsboard: GatewayGeneralConfig;
storage: {
type: StorageTypes;
read_records_count?: number;
max_records_count?: number;
data_folder_path?: string;
max_file_count?: number;
max_read_records_count?: number;
max_records_per_file?: number;
data_file_path?: string;
messages_ttl_check_in_hours?: number;
messages_ttl_in_days?: number;
};
grpc: {
enabled: boolean;
serverPort: number;
keepAliveTimeMs: number;
keepAliveTimeoutMs: number;
keepalivePermitWithoutCalls: boolean;
maxPingsWithoutData: number;
minTimeBetweenPingsMs: number;
minPingIntervalWithoutDataMs: number;
};
storage: GatewayStorageConfig;
grpc: GatewayGRPCConfig;
connectors?: GatewayConnector[];
logs: GatewayLogsConfig;
}
export interface GatewayGRPCConfig {
enabled: boolean;
serverPort: number;
keepAliveTimeMs: number;
keepAliveTimeoutMs: number;
keepalivePermitWithoutCalls: boolean;
maxPingsWithoutData: number;
minTimeBetweenPingsMs: number;
minPingIntervalWithoutDataMs: number;
}
export interface GatewayStorageConfig {
type: StorageTypes;
read_records_count?: number;
max_records_count?: number;
data_folder_path?: string;
max_file_count?: number;
max_read_records_count?: number;
max_records_per_file?: number;
data_file_path?: string;
messages_ttl_check_in_hours?: number;
messages_ttl_in_days?: number;
}
export interface GatewayGeneralConfig {
host: string;
port: number;

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts

@ -256,10 +256,11 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
private getMappingValue(value: ConnectorMapping): MappingValue {
switch (this.mappingType) {
case MappingType.DATA:
const converterType = ConvertorTypeTranslationsMap.get((value as ConverterConnectorMapping).converter?.type);
return {
topicFilter: (value as ConverterConnectorMapping).topicFilter,
QoS: (value as ConverterConnectorMapping).subscriptionQos,
converter: this.translate.instant(ConvertorTypeTranslationsMap.get((value as ConverterConnectorMapping).converter?.type) || '')
converter: converterType ? this.translate.instant(converterType) : ''
};
case MappingType.REQUESTS:
let details: string;

56
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -148,6 +148,62 @@
</mat-form-field>
</div>
</div>
<div *ngIf="showModifiersMap.get(keyControl.get('id').value)" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="enableModifiersControlMap.get(keyControl.get('id').value).value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle
fxLayoutAlign="center"
[formControl]="enableModifiersControlMap.get(keyControl.get('id').value)"
class="mat-slide"
(click)="$event.stopPropagation()"
>
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.modifier' | translate }}">
{{ 'gateway.modifier' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-flex no-gap">
<div class="tb-form-row column-xs tb-flex full-width" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="modifierType">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="ModifierTypesMap.get(keyControl.get('modifierType').value)?.icon"></mat-icon>
<span>{{ ModifierTypesMap.get(keyControl.get('modifierType').value)?.name | translate}}</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let modifierType of modifierTypes" [value]="modifierType">
<mat-icon class="tb-mat-20" svgIcon="{{ ModifierTypesMap.get(modifierType).icon }}">
</mat-icon>
<span>{{ ModifierTypesMap.get(modifierType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.value</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex tb-suffix-absolute">
<input matInput required formControlName="modifierValue" step="0.1" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.modifier-invalid') | translate"
*ngIf="keyControl.get('modifierValue').hasError('pattern') &&
keyControl.get('modifierValue').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</mat-expansion-panel>
</div>
<div *ngIf="isMaster" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<div class="tb-flex no-gap">

80
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -18,6 +18,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu
import {
AbstractControl,
FormArray,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
@ -27,12 +28,15 @@ import {
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
ModbusDataType,
ModbusEditableDataTypes,
ModbusEditableDataTypes, ModbusFormValue,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
ModbusValueKey,
ModifierType,
ModifierTypesMap,
noLeadTrailSpacesRegex,
nonZeroFloat,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
@ -75,13 +79,18 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
keysListFormArray: FormArray<UntypedFormGroup>;
modbusDataTypes = Object.values(ModbusDataType);
modifierTypes: ModifierType[] = Object.values(ModifierType);
withFunctionCode = true;
withReportStrategy = true;
enableModifiersControlMap = new Map<string, FormControl<boolean>>();
showModifiersMap = new Map<string, boolean>();
functionCodesMap = new Map();
defaultFunctionCodes = [];
readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly ModifierTypesMap = ModifierTypesMap;
private destroy$ = new Subject<void>();
@ -111,6 +120,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
}
addKey(): void {
const id = generateSecret(5);
const dataKeyFormGroup = this.fb.group({
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -119,9 +129,14 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
objectsCount: [1, [Validators.required]],
functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]],
reportStrategy: [{ value: null, disabled: !this.withReportStrategy }],
id: [{value: generateSecret(5), disabled: true}],
modifierType: [{ value: ModifierType.MULTIPLIER, disabled: true }],
modifierValue: [{ value: 1, disabled: true }, [Validators.pattern(nonZeroFloat)]],
id: [{value: id, disabled: true}],
});
this.showModifiersMap.set(id, false);
this.enableModifiersControlMap.set(id, this.fb.control(false));
this.observeKeyDataType(dataKeyFormGroup);
this.observeEnableModifier(dataKeyFormGroup);
this.keysListFormArray.push(dataKeyFormGroup);
}
@ -143,9 +158,11 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
}
private getFormValue(): ModbusValue[] {
return this.withReportStrategy
return this.mapKeysWithModifier(
this.withReportStrategy
? this.cleanUpEmptyStrategies(this.keysListFormArray.value)
: this.keysListFormArray.value;
: this.keysListFormArray.value
);
}
private cleanUpEmptyStrategies(values: ModbusValue[]): ModbusValue[] {
@ -155,6 +172,16 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
});
}
private mapKeysWithModifier(values: Array<ModbusFormValue>): Array<ModbusValue> {
return values.map((keyData, i) => {
if (this.showModifiersMap.get(this.keysListFormArray.controls[i].get('id').value)) {
const { modifierType, modifierValue, ...value } = keyData;
return modifierType ? { ...value, [modifierType]: modifierValue } : value;
}
return keyData;
});
}
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
@ -162,6 +189,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
values.forEach(value => {
const dataKeyFormGroup = this.createDataKeyFormGroup(value);
this.observeKeyDataType(dataKeyFormGroup);
this.observeEnableModifier(dataKeyFormGroup);
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type));
keysControlGroups.push(dataKeyFormGroup);
@ -172,7 +200,12 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
}
private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup {
const { tag, value, type, address, objectsCount, functionCode, reportStrategy } = modbusValue;
const { tag, value, type, address, objectsCount, functionCode, multiplier, divider, reportStrategy } = modbusValue;
const id = generateSecret(5);
const showModifier = this.shouldShowModifier(type);
this.showModifiersMap.set(id, showModifier);
this.enableModifiersControlMap.set(id, this.fb.control((multiplier || divider) && showModifier));
return this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -181,20 +214,55 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]],
functionCode: [{ value: functionCode, disabled: !this.withFunctionCode }, [Validators.required]],
id: [{ value: generateSecret(5), disabled: true }],
modifierType: [{
value: divider ? ModifierType.DIVIDER : ModifierType.MULTIPLIER,
disabled: !this.enableModifiersControlMap.get(id).value
}],
modifierValue: [
{ value: multiplier ?? divider ?? 1, disabled: !this.enableModifiersControlMap.get(id).value },
[Validators.pattern(nonZeroFloat)]
],
id: [{ value: id, disabled: true }],
reportStrategy: [{ value: reportStrategy, disabled: !this.withReportStrategy }],
});
}
private shouldShowModifier(type: ModbusDataType): boolean {
return !this.isMaster
&& (this.keysType === ModbusValueKey.ATTRIBUTES || this.keysType === ModbusValueKey.TIMESERIES)
&& (!this.ModbusEditableDataTypes.includes(type));
}
private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.ModbusEditableDataTypes.includes(dataType)) {
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
const withModifier = this.shouldShowModifier(dataType);
this.showModifiersMap.set(keyFormGroup.get('id').value, withModifier);
this.updateFunctionCodes(keyFormGroup, dataType);
});
}
private observeEnableModifier(keyFormGroup: FormGroup): void {
this.enableModifiersControlMap.get(keyFormGroup.get('id').value).valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(showModifier => this.toggleModifierControls(keyFormGroup, showModifier));
}
private toggleModifierControls(keyFormGroup: FormGroup, enable: boolean): void {
const modifierTypeControl = keyFormGroup.get('modifierType');
const modifierValueControl = keyFormGroup.get('modifierValue');
if (enable) {
modifierTypeControl.enable();
modifierValueControl.enable();
} else {
modifierTypeControl.disable();
modifierValueControl.disable();
}
}
private updateFunctionCodes(keyFormGroup: FormGroup, dataType: ModbusDataType): void {
const functionCodes = this.getFunctionCodes(dataType);
this.functionCodesMap.set(keyFormGroup.get('id').value, functionCodes);

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.html

@ -54,7 +54,9 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tbTruncateWithTooltip translate>gateway.security</div>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.security-policy' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.security-policy' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="security">

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -297,13 +297,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
private hasSameConfig(sharedDataConfigJson: ConnectorBaseInfo, connectorDataConfigJson: ConnectorBaseInfo): boolean {
const { name, id, enableRemoteLogging, logLevel, reportStrategy, ...sharedDataConfig } = sharedDataConfigJson;
const { name, id, enableRemoteLogging, logLevel, reportStrategy, configVersion, ...sharedDataConfig } = sharedDataConfigJson;
const {
name: connectorName,
id: connectorId,
enableRemoteLogging: connectorEnableRemoteLogging,
logLevel: connectorLogLevel,
reportStrategy: connectorReportStrategy,
configVersion: connectorConfigVersion,
...connectorConfig
} = connectorDataConfigJson;

33
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -19,6 +19,7 @@ import { AttributeData } from '@shared/models/telemetry/telemetry.models';
export const noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/;
export const integerRegex = /^[-+]?\d+$/;
export const nonZeroFloat = /^-?(?!0(\.0+)?$)\d+(\.\d+)?$/;
export enum StorageTypes {
MEMORY = 'memory',
@ -208,6 +209,7 @@ export interface ConnectorBaseInfo {
id: string;
enableRemoteLogging: boolean;
logLevel: GatewayLogLevel;
configVersion: string | number;
reportStrategy?: ReportStrategyConfig;
}
@ -913,6 +915,30 @@ export enum MappingValueType {
BOOLEAN = 'boolean'
}
export enum ModifierType {
DIVIDER = 'divider',
MULTIPLIER = 'multiplier',
}
export const ModifierTypesMap = new Map<ModifierType, ValueTypeData>(
[
[
ModifierType.DIVIDER,
{
name: 'gateway.divider',
icon: 'mdi:division'
}
],
[
ModifierType.MULTIPLIER,
{
name: 'gateway.multiplier',
icon: 'mdi:multiplication'
}
],
]
);
export const mappingValueTypesMap = new Map<MappingValueType, ValueTypeData>(
[
[
@ -1162,6 +1188,13 @@ export interface ModbusValue {
address: number;
value?: string;
reportStrategy?: ReportStrategyConfig;
multiplier?: number;
divider?: number;
}
export interface ModbusFormValue extends ModbusValue {
modifierType?: ModifierType;
modifierValue?: string;
}
export interface ModbusSecurity {

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts

@ -35,9 +35,10 @@ import {
export class OpcVersionMappingUtil {
static mapServerToUpgradedVersion(server: LegacyServerConfig): ServerConfig {
const { mapping, disableSubscriptions, ...restServer } = server;
const { mapping, disableSubscriptions, pollPeriodInMillis, ...restServer } = server;
return {
...restServer,
pollPeriodInMillis: pollPeriodInMillis ?? 5000,
enableSubscriptions: !disableSubscriptions,
};
}

17
ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, Input, NgZone, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
@ -81,7 +81,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
private utilsService: UtilsService,
private elementRef: ElementRef,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,) {
private sanitizer: DomSanitizer,
private zone: NgZone) {
super(store);
}
@ -101,11 +102,13 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
}
this.widgetResize$ = new ResizeObserver(() => {
const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250;
if (showHideBadgeContainer !== this.showBadgeContainer) {
this.showBadgeContainer = showHideBadgeContainer;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250;
if (showHideBadgeContainer !== this.showBadgeContainer) {
this.showBadgeContainer = showHideBadgeContainer;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);

4
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -189,7 +189,9 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
this.buildForm();
this.ctx.updateWidgetParams();
this.formResize$ = new ResizeObserver(() => {
this.resize();
this.ngZone.run(() => {
this.resize();
});
});
this.formResize$.observe(this.formContainerRef.nativeElement);
}

20
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts

@ -15,11 +15,12 @@
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Injector,
Input,
Input, NgZone, OnDestroy,
OnInit,
StaticProvider,
ViewChild,
@ -100,7 +101,7 @@ interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDes
styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss']
})
export class PersistentTableComponent extends PageComponent implements OnInit {
export class PersistentTableComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit {
@Input()
ctx: WidgetContext;
@ -144,7 +145,8 @@ export class PersistentTableComponent extends PageComponent implements OnInit {
private dialogService: DialogService,
private deviceService: DeviceService,
private dialog: MatDialog,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
private zone: NgZone) {
super(store);
}
@ -158,11 +160,13 @@ export class PersistentTableComponent extends PageComponent implements OnInit {
this.ctx.updateWidgetParams();
if (this.displayPagination) {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}

28
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -45,8 +45,10 @@ import {
deepClone,
formatValue,
guid,
isDefinedAndNotNull, isFirefox,
isNumeric, isSafari,
isDefinedAndNotNull,
isFirefox,
isNumeric,
isSafari,
isUndefined,
isUndefinedOrNull,
mergeDeep,
@ -1216,8 +1218,8 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
return this;
}
public ease(easing: string): this {
this._easing = easing;
public ease(easing: string | EasingLiteral): this {
this._easing = this.easingLiteralToCssEasing(easing);
this.updateAnimationStyle('animation-timing-function', this._easing);
return this;
}
@ -1551,10 +1553,10 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
transform = deepClone(transform);
Object.assign(transform, inputTransform);
}
return {
'transform-origin': `${transform.originX}px ${transform.originY}px`,
transform: `translate(${transform.translateX}px, ${transform.translateY}px) ` +
`skewX(${transform.b}deg) skewY(${transform.c}deg) ` +
`scale(${transform.scaleX}, ${transform.scaleY}) ` +
`rotate(${transform.rotate}deg)`};
}
@ -1563,6 +1565,22 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
const factor = Math.pow(10, digits);
return Math.round((num + Number.EPSILON) * factor) / factor;
}
private easingLiteralToCssEasing(easing: string | EasingLiteral): string {
switch (easing) {
case '<>':
return 'ease-in-out';
case '-':
return 'linear';
case '>':
return 'ease-out';
case '<':
return 'ease-in';
default:
return easing;
}
}
}
class JsScadaSymbolAnimation implements ScadaSymbolAnimation {

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

@ -20,7 +20,7 @@ import {
Component,
ElementRef,
Injector,
Input,
Input, NgZone,
OnDestroy,
OnInit,
QueryList,
@ -224,7 +224,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private domSanitizer: DomSanitizer,
private datePipe: DatePipe,
private cd: ChangeDetectorRef,
private fb: FormBuilder) {
private fb: FormBuilder,
private zone: NgZone) {
super(store);
}
@ -249,11 +250,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
}
);
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}

17
ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts

@ -19,15 +19,19 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ComponentRef,
ElementRef,
EventEmitter,
HostBinding,
Input, OnChanges,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2, SimpleChanges,
ViewChild, ViewContainerRef,
Renderer2,
SimpleChanges,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
@ -38,9 +42,9 @@ import { SafeStyle } from '@angular/platform-browser';
import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
export enum WidgetComponentActionType {
MOUSE_DOWN,
@ -173,7 +177,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
if (this.cssClass) {
this.utils.clearCssElement(this.renderer, this.cssClass);
}
if (this.editWidgetActionsTooltip) {
if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) {
this.editWidgetActionsTooltip.destroy();
}
}
@ -255,6 +259,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
private initEditWidgetActionTooltip(parent: HTMLElement) {
let componentRef: ComponentRef<EditWidgetActionsTooltipComponent>;
from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({
parent: $(parent),
@ -305,7 +310,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
});
this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance');
const componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent);
componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent);
componentRef.instance.container = this;
componentRef.instance.viewInited.subscribe(() => {
if (this.editWidgetActionsTooltip.status().open) {

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

@ -546,7 +546,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
});
} else {
this.onInit(true);
this.ngZone.run(() => {
this.onInit(true);
});
}
}
}

30
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -706,15 +706,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
return Math.floor(res);
}
set x(x: number) {
if (!this.dashboard.isMobileSize && this.dashboard.isEdit) {
if (this.widgetLayout) {
this.widgetLayout.col = x;
} else {
this.widget.col = x;
}
}
}
set x(_: number) {}
@enumerable(true)
get y(): number {
@ -727,15 +719,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
return Math.floor(res);
}
set y(y: number) {
if (!this.dashboard.isMobileSize && this.dashboard.isEdit) {
if (this.widgetLayout) {
this.widgetLayout.row = y;
} else {
this.widget.row = y;
}
}
}
set y(_: number) {}
get preserveAspectRatio(): boolean {
if (!this.dashboard.isMobileSize && this.widgetLayout) {
@ -833,4 +817,14 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
}
return order;
}
updatePosition(x: number, y: number) {
if (this.widgetLayout) {
this.widgetLayout.col = x;
this.widgetLayout.row = y;
} else {
this.widget.col = x;
this.widget.row = y;
}
}
}

7
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts

@ -14,13 +14,13 @@
/// limitations under the License.
///
import { Component, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { AfterViewInit, Component, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
import { FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
import { getProviderHelpLink, OAuth2Client } from '@shared/models/oauth2.models';
import { OAuth2Service } from '@core/http/oauth2.service';
import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component';
@ -32,7 +32,7 @@ import { ErrorStateMatcher } from '@angular/material/core';
providers: [{provide: ErrorStateMatcher, useExisting: ClientDialogComponent}],
styleUrls: []
})
export class ClientDialogComponent extends DialogComponent<ClientDialogComponent, OAuth2Client> implements OnDestroy {
export class ClientDialogComponent extends DialogComponent<ClientDialogComponent, OAuth2Client> implements OnDestroy, AfterViewInit {
submitted = false;
@ -41,7 +41,6 @@ export class ClientDialogComponent extends DialogComponent<ClientDialogComponent
constructor(protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<ClientDialogComponent, OAuth2Client>,
private fb: FormBuilder,
private oauth2Service: OAuth2Service,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store, router, dialogRef);

13
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core';
import { ChangeDetectorRef, Component, Inject, Input, OnDestroy, Optional } from '@angular/core';
import { EntityComponent } from '@home/components/entity/entity.component';
import {
ClientAuthenticationMethod,
@ -54,7 +54,7 @@ import { coerceBoolean } from '@app/shared/decorators/coercion';
templateUrl: './client.component.html',
styleUrls: ['./client.component.scss']
})
export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAuth2ClientInfo> {
export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAuth2ClientInfo> implements OnDestroy {
@Input()
@coerceBoolean()
@ -109,7 +109,8 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
protected translate: TranslateService,
private oauth2Service: OAuth2Service,
@Optional() @Inject('entity') protected entityValue: OAuth2Client,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>,
@Optional() @Inject('entitiesTableConfig')
protected entitiesTableConfigValue: EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>,
protected cd: ChangeDetectorRef,
public fb: UntypedFormBuilder) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
@ -185,13 +186,13 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
if (entity.scope.length === scopeControls.length) {
scopeControls.patchValue(entity.scope, {emitEvent: false});
} else {
const scopeControls: Array<AbstractControl> = [];
const newScopeControls: Array<AbstractControl> = [];
if (entity.scope) {
for (const scope of entity.scope) {
scopeControls.push(this.fb.control(scope, [Validators.required]));
newScopeControls.push(this.fb.control(scope, [Validators.required]));
}
}
this.entityForm.setControl('scope', this.fb.array(scopeControls));
this.entityForm.setControl('scope', this.fb.array(newScopeControls));
}
}

21
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts

@ -27,7 +27,6 @@ import {
OAuth2ClientInfo,
platformTypeTranslations
} from '@shared/models/oauth2.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
@ -40,11 +39,11 @@ import { PageLink } from '@shared/models/page/page-link';
@Injectable()
export class ClientsTableConfigResolver implements Resolve<EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>> {
private readonly config: EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo> = new EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>();
private readonly config: EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo> =
new EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>();
constructor(private translate: TranslateService,
private datePipe: DatePipe,
private utilsService: UtilsService,
private oauth2Service: OAuth2Service) {
this.config.tableTitle = this.translate.instant('admin.oauth2.clients');
this.config.selectionEnabled = false;
@ -53,9 +52,7 @@ export class ClientsTableConfigResolver implements Resolve<EntityTableConfig<OAu
this.config.entityTranslations = entityTypeTranslations.get(EntityType.OAUTH2_CLIENT);
this.config.entityResources = {
helpLinkId: null,
helpLinkIdForEntity(entity: OAuth2Client): string {
return getProviderHelpLink(entity.additionalInfo.providerName);
}
helpLinkIdForEntity: (entity: OAuth2Client) => getProviderHelpLink(entity.additionalInfo.providerName)
};
this.config.entityComponent = ClientComponent;
this.config.headerComponent = ClientTableHeaderComponent;
@ -66,20 +63,16 @@ export class ClientsTableConfigResolver implements Resolve<EntityTableConfig<OAu
new DateEntityTableColumn<OAuth2ClientInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),
new EntityTableColumn<OAuth2ClientInfo>('title', 'admin.oauth2.title', '350px'),
new EntityTableColumn<OAuth2ClientInfo>('platforms', 'admin.oauth2.allowed-platforms', '100%',
(clientInfo) => {
return clientInfo.platforms && clientInfo.platforms.length ?
clientInfo.platforms.map(platform => this.translate.instant(platformTypeTranslations.get(platform))).join(', ') :
this.translate.instant('admin.oauth2.all-platforms');
}, () => ({}), false)
(clientInfo) => clientInfo.platforms && clientInfo.platforms.length ?
clientInfo.platforms.map(platform => this.translate.instant(platformTypeTranslations.get(platform))).join(', ') :
this.translate.instant('admin.oauth2.all-platforms'), () => ({}), false)
);
this.config.deleteEntityTitle = (client) => this.translate.instant('admin.oauth2.delete-client-title', {clientName: client.title});
this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-client-text');
this.config.entitiesFetchFunction = pageLink => this.oauth2Service.findTenantOAuth2ClientInfos(pageLink);
this.config.loadEntity = id => this.oauth2Service.getOAuth2ClientById(id.id);
this.config.saveEntity = client => {
return this.oauth2Service.saveOAuth2Client(client);
}
this.config.saveEntity = client => this.oauth2Service.saveOAuth2Client(client);
this.config.deleteEntity = id => this.oauth2Service.deleteOauth2Client(id.id);
}

18
ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts

@ -33,7 +33,7 @@ import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.compone
import { isEqual } from '@core/utils';
import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component';
import { Direction } from '@app/shared/models/page/sort-order';
import { map } from 'rxjs';
import { map, Observable, of, mergeMap } from 'rxjs';
@Injectable()
export class DomainTableConfigResolver implements Resolve<EntityTableConfig<DomainInfo>> {
@ -87,18 +87,22 @@ export class DomainTableConfigResolver implements Resolve<EntityTableConfig<Doma
this.config.loadEntity = id => this.domainService.getDomainInfoById(id.id);
this.config.saveEntity = (domain, originalDomain) => {
const clientsIds = domain.oauth2ClientInfos as Array<string> || [];
let clientsTask: Observable<void>;
if (domain.id && !isEqual(domain.oauth2ClientInfos?.sort(),
originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) {
this.domainService.updateOauth2Clients(domain.id.id, clientsIds).subscribe();
clientsTask = this.domainService.updateOauth2Clients(domain.id.id, clientsIds);
} else {
clientsTask = of(null);
}
delete domain.oauth2ClientInfos;
return this.domainService.saveDomain(domain, domain.id ? [] : clientsIds).pipe(
map(domain => {
(domain as DomainInfo).oauth2ClientInfos = clientsIds;
return domain;
return clientsTask.pipe(
mergeMap(() => this.domainService.saveDomain(domain, domain.id ? [] : clientsIds)),
map(savedDomain => {
(savedDomain as DomainInfo).oauth2ClientInfos = clientsIds;
return savedDomain;
})
);
}
};
this.config.deleteEntity = id => this.domainService.deleteDomain(id.id);
}

5
ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html

@ -54,10 +54,9 @@
<tb-entity-list [entityType]="entityType.OAUTH2_CLIENT" formControlName="oauth2ClientInfos"
labelText="{{ 'admin.oauth2.clients' | translate }}"
placeholderText="{{ 'admin.oauth2.add-client' | translate }}">
<button #createTargetButton
mat-button color="primary" matSuffix
<button mat-button color="primary" matSuffix
[disabled]="!isEdit"
(click)="createClient($event, createTargetButton)">
(click)="createClient($event)">
<span style="white-space: nowrap">{{ 'admin.oauth2.create-new' | translate }}</span>
</button>
</tb-entity-list>

5
ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts

@ -26,7 +26,6 @@ import { Store } from '@ngrx/store';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { WINDOW } from '@core/services/window.service';
import { isDefinedAndNotNull } from '@core/utils';
import { MatButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component';
import { EntityType } from '@shared/models/entity-type.models';
@ -38,7 +37,7 @@ import { EntityType } from '@shared/models/entity-type.models';
})
export class DomainComponent extends EntityComponent<DomainInfo> {
private loginProcessingUrl: string = '';
private loginProcessingUrl = '';
entityType = EntityType;
@ -83,7 +82,7 @@ export class DomainComponent extends EntityComponent<DomainInfo> {
return domainName !== '' ? `${domainName}${this.loginProcessingUrl}` : '';
}
createClient($event: Event, button: MatButton) {
createClient($event: Event) {
if ($event) {
$event.stopPropagation();
$event.preventDefault();

22
ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts

@ -25,7 +25,6 @@ import {
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
@ -34,7 +33,7 @@ import { Direction } from '@app/shared/models/page/sort-order';
import { MobileAppService } from '@core/http/mobile-app.service';
import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component';
import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component';
import { map } from 'rxjs';
import { map, Observable, of, mergeMap } from 'rxjs';
@Injectable()
export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<MobileAppInfo>> {
@ -43,7 +42,6 @@ export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<M
constructor(private translate: TranslateService,
private datePipe: DatePipe,
private utilsService: UtilsService,
private mobileAppService: MobileAppService) {
this.config.tableTitle = this.translate.instant('admin.oauth2.mobile-apps');
this.config.selectionEnabled = false;
@ -66,7 +64,7 @@ export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<M
name: this.translate.instant('admin.oauth2.copy-mobile-app-secret'),
icon: 'content_copy',
style: {
'padding-top': '1px',
padding: '4px',
'font-size': '16px',
color: 'rgba(0,0,0,.87)'
},
@ -93,18 +91,22 @@ export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<M
this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id);
this.config.saveEntity = (mobileApp, originalMobileApp) => {
const clientsIds = mobileApp.oauth2ClientInfos as Array<string> || [];
let clientsTask: Observable<void>;
if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(),
originalMobileApp.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).sort())) {
this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds).subscribe();
clientsTask = this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds);
} else {
clientsTask = of(null);
}
delete mobileApp.oauth2ClientInfos;
return this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds).pipe(
map(mobileApp => {
(mobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds;
return mobileApp;
return clientsTask.pipe(
mergeMap(() => this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds)),
map(savedMobileApp => {
(savedMobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds;
return savedMobileApp;
})
);
}
};
this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id);
}

8
ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html

@ -28,6 +28,9 @@
<mat-error *ngIf="entityForm.get('pkgName').hasError('maxlength')">
{{ 'admin.oauth2.mobile-package-max-length' | translate }}
</mat-error>
<mat-error *ngIf="entityForm.get('pkgName').hasError('pattern')">
{{ 'admin.oauth2.mobile-package-spaces' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-row tb-standard-fields no-border no-padding">
@ -63,10 +66,9 @@
<tb-entity-list [entityType]="entityType.OAUTH2_CLIENT" formControlName="oauth2ClientInfos"
labelText="{{ 'admin.oauth2.clients' | translate }}"
placeholderText="{{ 'admin.oauth2.add-client' | translate }}">
<button #createTargetButton
mat-button color="primary" matSuffix
<button mat-button color="primary" matSuffix
[disabled]="!isEdit"
(click)="createClient($event, createTargetButton)">
(click)="createClient($event)">
<span style="white-space: nowrap">{{ 'admin.oauth2.create-new' | translate }}</span>
</button>
</tb-entity-list>

12
ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts

@ -19,14 +19,11 @@ import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { EntityComponent } from '@home/components/entity/entity.component';
import { MobileAppInfo } from '@shared/models/oauth2.models';
import { AppState } from '@core/core.state';
import { OAuth2Service } from '@core/http/oauth2.service';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
import { WINDOW } from '@core/services/window.service';
import { isDefinedAndNotNull, randomAlphanumeric } from '@core/utils';
import { MatButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component';
import { EntityType } from '@shared/models/entity-type.models';
@ -42,19 +39,18 @@ export class MobileAppComponent extends EntityComponent<MobileAppInfo> {
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
private oauth2Service: OAuth2Service,
@Inject('entity') protected entityValue: MobileAppInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<MobileAppInfo>,
protected cd: ChangeDetectorRef,
public fb: UntypedFormBuilder,
@Inject(WINDOW) private window: Window,
private dialog: MatDialog) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
buildForm(entity: MobileAppInfo): UntypedFormGroup {
return this.fb.group({
pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255)]],
pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255),
Validators.pattern(/^\S+$/)]],
appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)),
[Validators.required, this.base64Format]],
oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true,
@ -68,10 +64,10 @@ export class MobileAppComponent extends EntityComponent<MobileAppInfo> {
appSecret: entity.appSecret,
oauth2Enabled: entity.oauth2Enabled,
oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info)
})
});
}
createClient($event: Event, button: MatButton) {
createClient($event: Event) {
if ($event) {
$event.stopPropagation();
$event.preventDefault();

7
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts

@ -20,7 +20,7 @@ import {
Component,
ElementRef,
EventEmitter,
Input,
Input, NgZone,
OnChanges,
OnDestroy,
OnInit,
@ -102,7 +102,8 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
this.updateEditorMode(value);
}
constructor(private cd: ChangeDetectorRef) {
constructor(private cd: ChangeDetectorRef,
private zone: NgZone) {
}
ngOnInit(): void {
@ -128,7 +129,7 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
};
this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement,
this.tooltipsContainer.nativeElement,
this.tooltipsContainerComponent.viewContainerRef, this.editObjectCallbacks, this.readonly);
this.tooltipsContainerComponent.viewContainerRef, this.zone, this.editObjectCallbacks, this.readonly);
if (this.data) {
this.updateContent(this.data.scadaSymbolContent);
}

16
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -17,7 +17,7 @@
import { ImageResourceInfo } from '@shared/models/resource.models';
import * as svgjs from '@svgdotjs/svg.js';
import { Box, Element, Rect, Style, SVG, Svg, Timeline } from '@svgdotjs/svg.js';
import { ViewContainerRef } from '@angular/core';
import { NgZone, ViewContainerRef } from '@angular/core';
import { forkJoin, from } from 'rxjs';
import {
setupAddTagPanelTooltip,
@ -79,6 +79,7 @@ export class ScadaSymbolEditObject {
constructor(private rootElement: HTMLElement,
public tooltipsContainer: HTMLElement,
public viewContainerRef: ViewContainerRef,
private zone: NgZone,
private callbacks: ScadaSymbolEditObjectCallbacks,
public readonly: boolean) {
this.shapeResize$ = new ResizeObserver(() => {
@ -193,7 +194,9 @@ export class ScadaSymbolEditObject {
from(import('tooltipster')),
from(import('tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js'))
]).subscribe(() => {
this.setupElements();
this.zone.run(() => {
this.setupElements();
});
});
}
@ -826,6 +829,7 @@ export class ScadaSymbolElement {
private innerAddTagTooltipPosition(_instance: ITooltipsterInstance,
_helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition {
const distance = 10;
const parentRect = this.tooltipContainer[0].getBoundingClientRect();
switch (position.side) {
case 'right':
position.coord.top = this.tooltipMouseY - position.size.height / 2;
@ -851,6 +855,14 @@ export class ScadaSymbolElement {
position.target = this.tooltipMouseX;
break;
}
const rightOverflow = parentRect.right - (position.coord.left + position.size.width);
if (rightOverflow < 0) {
position.coord.left += rightOverflow;
}
const leftOverflow = parentRect.left - position.coord.left;
if (leftOverflow > 0) {
position.coord.left += leftOverflow;
}
return position;
}

11
ui-ngx/src/app/shared/components/contact.component.html

@ -20,27 +20,30 @@
formControlName="country"
(selectCountryCode)="changeCountry($event)">
</tb-country-autocomplete>
<div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px">
<mat-form-field class="mat-block">
<div class="tb-form-row column-xs tb-standard-fields no-border no-padding">
<mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label translate>contact.city</mat-label>
<input matInput formControlName="city">
<mat-error *ngIf="parentForm.get('city').hasError('maxlength')">
{{ 'contact.city-max-length' | translate }}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label translate>contact.state</mat-label>
<input matInput formControlName="state">
<mat-error *ngIf="parentForm.get('state').hasError('maxlength')">
{{ 'contact.state-max-length' | translate }}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label translate>contact.postal-code</mat-label>
<input matInput formControlName="zip">
<mat-error *ngIf="parentForm.get('zip').hasError('pattern')">
{{ 'contact.postal-code-invalid' | translate }}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<mat-form-field class="mat-block">

2
ui-ngx/src/app/shared/components/country-autocomplete.component.ts

@ -200,7 +200,7 @@ export class CountryAutocompleteComponent implements OnInit, ControlValueAccesso
private updateView(value: Country | null) {
if (this.modelValue?.name !== value?.name) {
this.modelValue = value;
this.propagateChange(this.modelValue);
this.propagateChange(this.modelValue?.name);
if (value) {
this.selectCountryCode.emit(value.iso2);
}

4
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html

@ -29,9 +29,7 @@
<div mat-dialog-content>
<div class="message-container">
<span>
{{ 'entity.version-conflict.link' | translate:
{ entityType: (entityTypeTranslations.get(entityId.entityType).type | translate) }
}}
{{ 'entity.version-conflict.link' | translate:{ entityType: entityTypeLabel | translate } }}
<a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>.
</span>
<br/>

5
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts

@ -42,8 +42,10 @@ interface EntityConflictDialogData {
export class EntityConflictDialogComponent {
entityId: EntityId;
entityTypeLabel: string;
readonly entityTypeTranslations = entityTypeTranslations;
private readonly defaultEntityLabel = 'entity.entity';
constructor(
@Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData,
@ -51,6 +53,9 @@ export class EntityConflictDialogComponent {
private importExportService: ImportExportService,
) {
this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as RuleChainMetaData).ruleChainId;
this.entityTypeLabel = entityTypeTranslations.has(this.entityId.entityType)
? (entityTypeTranslations.get(this.entityId.entityType).type)
: this.defaultEntityLabel;
}
onCancel(): void {

9
ui-ngx/src/app/shared/components/grid/scroll-grid.component.ts

@ -17,7 +17,7 @@
import {
AfterViewInit, ChangeDetectorRef,
Component,
Input,
Input, NgZone,
OnChanges, OnDestroy,
OnInit,
Renderer2,
@ -90,7 +90,8 @@ export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChang
constructor(private breakpointObserver: BreakpointObserver,
private cd: ChangeDetectorRef,
private renderer: Renderer2) {
private renderer: Renderer2,
private zone: NgZone) {
}
ngOnInit(): void {
@ -109,7 +110,9 @@ export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChang
this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'padding', this.gap + 'px');
if (!(typeof this.itemSize === 'number')) {
this.contentResize$ = new ResizeObserver(() => {
this.onContentResize();
this.zone.run(() => {
this.onContentResize();
});
});
this.contentResize$.observe(this.viewport._contentWrapper.nativeElement);
}

17
ui-ngx/src/app/shared/components/image/image-gallery.component.ts

@ -33,7 +33,7 @@ import {
ElementRef,
EventEmitter,
HostBinding,
Input,
Input, NgZone,
OnDestroy,
OnInit,
Output,
@ -196,7 +196,8 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
private importExportService: ImportExportService,
private elementRef: ElementRef,
private cd: ChangeDetectorRef,
private fb: FormBuilder) {
private fb: FormBuilder,
private zone: NgZone) {
super(store);
this.gridImagesFetchFunction = (pageSize, page, filter) => {
@ -361,11 +362,13 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
private initListMode() {
this.destroyListMode$ = new Subject<void>();
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
this.zone.run(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
});
this.widgetResize$.observe(this.elementRef.nativeElement);
if (this.pageMode) {

9
ui-ngx/src/app/shared/components/toggle-header.component.ts

@ -25,7 +25,7 @@ import {
ElementRef,
EventEmitter,
HostBinding,
Input,
Input, NgZone,
OnDestroy,
OnInit,
Output,
@ -213,7 +213,8 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterV
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private platform: Platform,
private breakpointObserver: BreakpointObserver) {
private breakpointObserver: BreakpointObserver,
private zone: NgZone) {
super(store);
}
@ -296,7 +297,9 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterV
private startObservePagination() {
this.toggleGroupResize$ = new ResizeObserver(() => {
this.updatePagination();
this.zone.run(() => {
this.updatePagination();
});
});
this.toggleGroupResize$.observe(this.toggleGroupContainer.nativeElement);
}

11
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -55,11 +55,7 @@ import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import {
EntityInfoData,
ImportEntitiesResultInfo,
ImportEntityData
} from '@shared/models/entity.models';
import { EntityInfoData, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
@ -85,6 +81,7 @@ import { selectUserSettingsProperty } from '@core/auth/auth.selectors';
import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
import { ExportableEntity } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { Customer } from '@shared/models/customer.model';
export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
@ -391,6 +388,10 @@ export class ImportExportService {
case EntityType.DASHBOARD:
preparedData = this.prepareDashboardExport(entityData as Dashboard);
break;
case EntityType.CUSTOMER:
preparedData = this.prepareExport({...entityData, name: (entityData as Customer).title});
(entityData as EntityInfoData).name = (entityData as Customer).title;
break;
default:
preparedData = this.prepareExport(entityData);
}

42
ui-ngx/src/app/shared/models/alarm.models.ts

@ -27,6 +27,7 @@ import { AlarmCommentId } from '@shared/models/id/alarm-comment-id';
import { UserId } from '@shared/models/id/user-id';
import { AlarmFilter } from '@shared/models/query/query.models';
import { HasTenantId } from '@shared/models/entity.models';
import { isNotEmptyStr } from '@core/utils';
export enum AlarmsMode {
ALL,
@ -127,7 +128,7 @@ export interface AlarmComment extends BaseData<AlarmCommentId> {
text: string;
edited?: boolean;
editedOn?: number;
}
};
}
export interface AlarmCommentInfo extends AlarmComment {
@ -173,9 +174,9 @@ export const simulatedAlarm: AlarmInfo = {
originatorName: 'Simulated',
originatorLabel: 'Simulated',
assignee: {
firstName: "",
lastName: "",
email: "test@example.com",
firstName: '',
lastName: '',
email: 'test@example.com',
},
originator: {
entityType: EntityType.DEVICE,
@ -350,3 +351,36 @@ export class AlarmQueryV2 {
}
}
export const getUserDisplayName = (alarmAssignee: AlarmAssignee | AlarmCommentInfo) => {
let displayName = '';
if (isNotEmptyStr(alarmAssignee.firstName) || isNotEmptyStr(alarmAssignee.lastName)) {
if (alarmAssignee.firstName) {
displayName += alarmAssignee.firstName;
}
if (alarmAssignee.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += alarmAssignee.lastName;
}
} else {
displayName = alarmAssignee.email;
}
return displayName;
};
export const getUserInitials = (alarmAssignee: AlarmAssignee): string => {
let initials = '';
if (isNotEmptyStr(alarmAssignee.firstName) || isNotEmptyStr(alarmAssignee.lastName)) {
if (alarmAssignee.firstName) {
initials += alarmAssignee.firstName.charAt(0);
}
if (alarmAssignee.lastName) {
initials += alarmAssignee.lastName.charAt(0);
}
} else {
initials += alarmAssignee.email.charAt(0);
}
return initials.toUpperCase();
};

2
ui-ngx/src/assets/dashboard/sys_admin_home_page.json

@ -1092,7 +1092,7 @@
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/tenants\">{{ 'tenant.tenants' | translate }}</a>\n </div>\n <a class=\"tb-hide-md-lg\" mat-flat-button color=\"primary\" routerLink=\"/tenants\" [queryParams]=\"{action: 'add'}\">{{ 'tenant.add' | translate }}</a>\n </div>\n <div class=\"tb-count\">${tenantsCount:0}</div>\n </div>\n <div class=\"tb-small-button-container tb-hide-not-md-lg\">\n <a class=\"tb-hide-not-md-lg\" mat-icon-button color=\"primary\" routerLink=\"/tenants\" [queryParams]=\"{action: 'add'}\"\n matTooltipPosition=\"above\"\n matTooltip=\"{{ 'tenant.add' | translate }}\">\n <mat-icon>add</mat-icon>\n </a>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-small-button-container {\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 28px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}\n"
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n white-space: nowrap;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n}\n\n.tb-small-button-container {\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 28px;\n line-height: 36px;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-count {\n font-size: 18px;\n line-height: 24px;\n }\n}\n"
},
"title": "Tenants",
"showTitleIcon": false,

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

@ -323,6 +323,7 @@
"mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.",
"mobile-package-unique": "Application package must be unique.",
"mobile-package-max-length": "Application package should be less than 256",
"mobile-package-spaces": "Application package should not contain spaces",
"mobile-app-secret": "Application secret",
"mobile-app-secret-hint": "Base64 encoded string representing at least 512 bits of data.",
"mobile-app-secret-required": "Application secret is required.",
@ -594,6 +595,7 @@
"assignee-last-name": "Assignee last name",
"assignee-email": "Assignee email",
"unassigned": "Unassigned",
"user-deleted": "User deleted",
"assignee-not-set": "All",
"status": "Status",
"alarm-details": "Alarm details",
@ -2934,6 +2936,7 @@
"details": "Details",
"delete-mapping-title": "Delete mapping?",
"delete-slave-title": "Delete slave?",
"divider": "Divider",
"download-configuration-file": "Download configuration file",
"download-docker-compose": "Download docker-compose.yml for your gateway",
"enable-remote-logging": "Enable remote logging",
@ -3089,8 +3092,11 @@
"min-pack-send-delay-required": "Min pack send delay is required",
"min-pack-send-delay-min": "Min pack send delay can not be less then 10",
"min-pack-send-delay-pattern": "Min pack send delay is not valid",
"multiplier": "Multiplier",
"mode": "Mode",
"model-name": "Model name",
"modifier": "Modifier",
"modifier-invalid": "Modifier is not valid",
"mqtt-version": "MQTT version",
"name": "Name",
"name-required": "Name is required.",
@ -3254,6 +3260,7 @@
"sub-check-period": "Subscription check period (ms)",
"sub-check-period-error": "Subscription check period should be at least {{min}} (ms).",
"security": "Security",
"security-policy": "Security policy",
"security-type": "Security type",
"security-types": {
"access-token": "Access Token",
@ -3467,6 +3474,7 @@
"file": "Your data will be stored in separated files and will be saved even after the gateway restart.",
"sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.",
"opc-timeout": "Timeout in milliseconds for connecting to OPC-UA server.",
"security-policy": "Security Policy defines the security mechanisms to be applied.",
"scan-period": "Period in milliseconds to rescan the server.",
"sub-check-period": "Period to check the subscriptions in the OPC-UA server.",
"enable-subscription": "If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInMillis.",
@ -3500,7 +3508,8 @@
"objects-count": "Depends on the selected type.",
"address": "Register address to verify.",
"key": "Key to be used as the attribute key for the platform instance.",
"data-keys": "For more information about function codes and data types click on help icon"
"data-keys": "For more information about function codes and data types click on help icon",
"modifier": "The retrieved value will be adjusted (by multiplying or dividing it) based on the specified modifier value."
}
}
},

2
ui-ngx/src/form.scss

@ -395,11 +395,13 @@
padding-top: 8px;
padding-bottom: 8px;
min-height: 40px;
height: 40px;
width: auto;
.mdc-text-field__input, .mat-mdc-select {
font-weight: 400;
font-size: 14px;
line-height: 20px;
vertical-align: middle;
}
}
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix,

3
ui-ngx/src/styles.scss

@ -912,6 +912,9 @@ mat-icon {
&.tb-mat-28 {
@include tb-mat-icon-size(28);
}
&.tb-mat-30 {
@include tb-mat-icon-size(30);
}
&.tb-mat-32 {
@include tb-mat-icon-size(32);
}

Loading…
Cancel
Save