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, type = ComponentType.EXTERNAL,
name = "rest api call", name = "rest api call",
configClazz = TbRestApiCallNodeConfiguration.class, configClazz = TbRestApiCallNodeConfiguration.class,
version = 2, version = 3,
nodeDescription = "Invoke REST API calls to external REST server", 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. " + 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." + "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 { public class TbRestApiCallNode extends TbAbstractExternalNode {
static final String PARSE_TO_PLAIN_TEXT = "parseToPlainText"; 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"; static final String TRIM_DOUBLE_QUOTES = "trimDoubleQuotes";
protected TbHttpClient httpClient; protected TbHttpClient httpClient;
@ -92,6 +93,11 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
hasChanges = true; hasChanges = true;
((ObjectNode) oldConfiguration).remove(List.of("useRedisQueueForMsgPersistence", "trimQueue", "maxQueueSize")); ((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; break;
default: default:
break; 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," + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}"), "\"credentials\": {\"type\": \"anonymous\"}," +
"\"maxInMemoryBufferSizeInKb\": 256}"),
// config for version 2 with upgrade from version 1 // config for version 2 with upgrade from version 1
Arguments.of(1, Arguments.of(1,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," + "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
@ -249,7 +250,24 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
"\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," + "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," + "\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," + "\"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) { } else if (loginError) {
this.showLoginErrorDialog(loginError); Promise.resolve().then(() => this.showLoginErrorDialog(loginError));
this.utils.updateQueryParam('loginError', null); this.utils.updateQueryParam('loginError', null);
return throwError(Error()); 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 { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.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 { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models'; import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults'; 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 { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service'; 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'); 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 this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges
.pipe( .pipe(
debounceTime(150), debounceTime(150),
map(value => { map(value => value ? (typeof value === 'string' ? value : '') : ''),
return value ? (typeof value === 'string' ? value : '') : ''
}),
distinctUntilChanged(), distinctUntilChanged(),
switchMap(name => this.fetchUsers(name)), switchMap(name => this.fetchUsers(name)),
share(), share(),
@ -123,7 +121,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
ngAfterViewInit() { ngAfterViewInit() {
setTimeout(() => { setTimeout(() => {
this.userInput.nativeElement.focus(); this.userInput.nativeElement.focus();
}, 0) }, 0);
} }
ngOnDestroy() { ngOnDestroy() {
@ -149,7 +147,7 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
this.alarmService.assignAlarm(this.alarmId, user.id.id, {ignoreLoading: true}).subscribe( this.alarmService.assignAlarm(this.alarmId, user.id.id, {ignoreLoading: true}).subscribe(
() => { () => {
this.reassigned = true; 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.alarmService.unassignAlarm(this.alarmId, {ignoreLoading: true}).subscribe(
() => { () => {
this.reassigned = true; 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}) return this.userService.getUsersForAssign(this.alarmId, pageLink, {ignoreLoading: true})
.pipe( .pipe(
catchError(() => of(emptyPageData<UserEmailInfo>())), catchError(() => of(emptyPageData<UserEmailInfo>())),
map(pageData => { map(pageData => pageData.data)
return pageData.data;
})
); );
} }
@ -191,42 +187,11 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
}, 0); }, 0);
} }
getUserDisplayName(entity: User) { getUserInitials(entity: UserEmailInfo): string {
let displayName = ''; return getUserInitials(entity);
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();
} }
getFullName(entity: User): string { getFullName(entity: UserEmailInfo): string {
let fullName = ''; let fullName = '';
if ((entity.firstName && entity.firstName.length > 0) || if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) { (entity.lastName && entity.lastName.length > 0)) {
@ -243,8 +208,8 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
return fullName; return fullName;
} }
getAvatarBgColor(entity: User) { getAvatarBgColor(entity: UserEmailInfo) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); 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 { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service'; 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'); export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken<any>('AlarmAssigneeSelectPanelData');
@ -165,39 +165,8 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit
}, 0); }, 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 { getUserInitials(entity: UserEmailInfo): string {
let initials = ''; return getUserInitials(entity);
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();
} }
getFullName(entity: UserEmailInfo): string { getFullName(entity: UserEmailInfo): string {
@ -218,7 +187,7 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit
} }
getAvatarBgColor(entity: UserEmailInfo) { 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"> subscriptSizing="dynamic">
<mat-label translate>alarm.assignee</mat-label> <mat-label translate>alarm.assignee</mat-label>
<input matInput readonly [value]="getAssignee()"> <input matInput readonly [value]="getAssignee()">
<span *ngIf="alarm?.assigneeId" matPrefix class="user-avatar" <span *ngIf="userAssigned; else unassigned" matPrefix class="user-avatar"
[style.backgroundColor]="getAvatarBgColor(alarm.assignee)"> [style.background-color]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }} {{ getUserInitials(alarm.assignee) }}
</span> </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-icon matSuffix>arrow_drop_down</mat-icon>
</mat-form-field> </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 { Component, EventEmitter, Injector, Input, Output, StaticProvider, ViewContainerRef } from '@angular/core';
import { UtilsService } from '@core/services/utils.service'; 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 { import {
ALARM_ASSIGNEE_PANEL_DATA, ALARM_ASSIGNEE_PANEL_DATA,
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
@ -25,6 +25,7 @@ import {
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { isNotEmptyStr } from '@core/utils';
@Component({ @Component({
selector: 'tb-alarm-assignee', selector: 'tb-alarm-assignee',
@ -41,6 +42,8 @@ export class AlarmAssigneeComponent {
@Output() @Output()
alarmReassigned = new EventEmitter<boolean>(); alarmReassigned = new EventEmitter<boolean>();
userAssigned: boolean;
constructor(private utilsService: UtilsService, constructor(private utilsService: UtilsService,
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
@ -49,68 +52,22 @@ export class AlarmAssigneeComponent {
getAssignee() { getAssignee() {
if (this.alarm) { if (this.alarm) {
if (this.alarm.assignee) { this.userAssigned = this.alarm.assigneeId && ((isNotEmptyStr(this.alarm.assignee?.firstName) ||
return this.getUserDisplayName(this.alarm.assignee); isNotEmptyStr(this.alarm.assignee?.lastName)) || isNotEmptyStr(this.alarm.assignee?.email));
if (this.userAssigned) {
return getUserDisplayName(this.alarm.assignee);
} else { } else {
return this.translateService.instant('alarm.unassigned'); return this.translateService.instant(this.alarm.assigneeId ? 'alarm.user-deleted' : '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);
} }
} else {
initials += entity.email.charAt(0);
} }
return initials.toUpperCase();
} }
getFullName(entity: AlarmAssignee): string { getUserInitials(alarmAssignee: AlarmAssignee): string {
let fullName = ''; return getUserInitials(alarmAssignee);
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;
} }
getAvatarBgColor(entity: 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) { 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" *ngIf="!displayDataElement.edit; else commentEditing"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)" (mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
(mouseleave)="onCommentMouseLeave(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"> [style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }} {{ getUserInitials(displayDataElement.displayName) }}
</div> </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 fxFlex fxLayout="column" fxLayoutGap="5px">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center"> <div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<span class="user-name">{{ displayDataElement.displayName }}</span> <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; background-color: $primary-color;
border: $border; border: $border;
border-bottom: none; border-bottom: none;
border-radius: 8px 8px 0px 0px; border-radius: 8px 8px 0 0;
&.activity-only { &.activity-only {
border: none; border: none;
@ -94,7 +94,7 @@ $border: 1px solid mat.get-color-from-palette($tb-primary);
&.asc { &.asc {
border-bottom: $border; 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 { &.activity-only {
border: none; border: none;
@ -116,7 +116,7 @@ $border: 1px solid mat.get-color-from-palette($tb-primary);
.comments-container { .comments-container {
border: $border; border: $border;
border-top: none; border-top: none;
border-radius: 0px 0px 8px 8px; border-radius: 0 0 8px 8px;
&.activity-only { &.activity-only {
border: none; border: none;
@ -134,6 +134,11 @@ $border: 1px solid mat.get-color-from-palette($tb-primary);
&:hover { &:hover {
background-color: $primary-color; background-color: $primary-color;
} }
.mat-icon {
align-self: start;
color: rgba(0, 0, 0, 0.38);
}
} }
.user-avatar { .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 { AlarmCommentService } from '@core/http/alarm-comment.service';
import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service'; 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 { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors';
import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { map } from 'rxjs/operators'; 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 { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { isNotEmptyStr } from '@core/utils';
interface AlarmCommentsDisplayData { interface AlarmCommentsDisplayData {
commentId?: string, commentId?: string;
displayName?: string, displayName?: string;
createdTime: string, createdTime: string;
createdDateAgo?: string, createdDateAgo?: string;
edit?: boolean, edit?: boolean;
isEdited?: boolean, isEdited?: boolean;
editedTime?: string; editedTime?: string;
editedDateAgo?: string, editedDateAgo?: string;
showActions?: boolean, showActions?: boolean;
commentText?: string, commentText?: string;
isSystemComment?: boolean, isSystemComment?: boolean;
avatarBgColor?: string avatarBgColor?: string;
userExists?: boolean;
} }
@Component({ @Component({
@ -58,7 +60,7 @@ export class AlarmCommentComponent implements OnInit {
alarmId: string; alarmId: string;
@Input() @Input()
alarmActivityOnly: boolean = false; alarmActivityOnly = false;
authUser: AuthUser; authUser: AuthUser;
@ -73,11 +75,11 @@ export class AlarmCommentComponent implements OnInit {
direction: Direction.DESC direction: Direction.DESC
}; };
editMode: boolean = false; editMode = false;
userDisplayName$ = this.store.pipe( userDisplayName$ = this.store.pipe(
select(selectUserDetails), select(selectUserDetails),
map((user) => this.getUserDisplayName(user)) map((user) => getUserDisplayName(user))
); );
currentUserDisplayName: string; currentUserDisplayName: string;
@ -115,15 +117,18 @@ export class AlarmCommentComponent implements OnInit {
(pagedData) => { (pagedData) => {
this.alarmComments = pagedData.data; this.alarmComments = pagedData.data;
this.displayData.length = 0; this.displayData.length = 0;
for (let alarmComment of pagedData.data) { for (const alarmComment of pagedData.data) {
let displayDataElement = {} as AlarmCommentsDisplayData; const displayDataElement = {} as AlarmCommentsDisplayData;
displayDataElement.createdTime = this.datePipe.transform(alarmComment.createdTime, 'yyyy-MM-dd HH:mm:ss'); displayDataElement.createdTime = this.datePipe.transform(alarmComment.createdTime, 'yyyy-MM-dd HH:mm:ss');
displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime); displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime);
displayDataElement.commentText = alarmComment.comment.text; displayDataElement.commentText = alarmComment.comment.text;
displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM; displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM;
if (alarmComment.type === AlarmCommentType.OTHER) { if (alarmComment.type === AlarmCommentType.OTHER) {
displayDataElement.commentId = alarmComment.id.id; 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.edit = false;
displayDataElement.isEdited = alarmComment.comment.edited; displayDataElement.isEdited = alarmComment.comment.edited;
displayDataElement.editedTime = this.datePipe.transform(alarmComment.comment.editedOn, 'yyyy-MM-dd HH:mm:ss'); 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); this.displayData.push(displayDataElement);
} }
} }
) );
} }
changeSortDirection() { changeSortDirection() {
let currentDirection = this.alarmCommentSortOrder.direction; const currentDirection = this.alarmCommentSortOrder.direction;
this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC; this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC;
this.loadAlarmComments(); this.loadAlarmComments();
} }
exportAlarmActivity() { 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()); this.importExportService.exportCsv(this.getDataForExport(), fileName.toLowerCase());
} }
@ -162,7 +167,7 @@ export class AlarmCommentComponent implements OnInit {
comment: { comment: {
text: commentInputValue text: commentInputValue
} }
} };
this.doSave(comment); this.doSave(comment);
this.clearCommentInput(); this.clearCommentInput();
} }
@ -185,7 +190,7 @@ export class AlarmCommentComponent implements OnInit {
() => { () => {
this.loadAlarmComments(); this.loadAlarmComments();
} }
) );
} }
editComment(commentId: string): void { editComment(commentId: string): void {
@ -217,18 +222,18 @@ export class AlarmCommentComponent implements OnInit {
.subscribe(() => { .subscribe(() => {
this.loadAlarmComments(); this.loadAlarmComments();
} }
) );
} }
} }
) );
} }
getSortDirectionIcon() { 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() { 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'; 'alarm-activity.oldest-first';
return this.translate.instant(text); return this.translate.instant(text);
} }
@ -264,25 +269,6 @@ export class AlarmCommentComponent implements OnInit {
return this.utilsService.stringToHslColor(userDisplayName, 40, 60); 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 { getAlarmCommentFormControl(): AbstractControl {
return this.alarmCommentFormGroup.get('alarmComment'); return this.alarmCommentFormGroup.get('alarmComment');
} }
@ -308,16 +294,16 @@ export class AlarmCommentComponent implements OnInit {
} }
private getDataForExport() { private getDataForExport() {
let dataToExport = []; const dataToExport = [];
for (let row of this.displayData) { for (const row of this.displayData) {
let exportRow = { const exportRow = {
[this.translate.instant('alarm-activity.author')]: row.isSystemComment ? [this.translate.instant('alarm-activity.author')]: row.isSystemComment ?
this.translate.instant('alarm-activity.system') : row.displayName, this.translate.instant('alarm-activity.system') : row.displayName,
[this.translate.instant('alarm-activity.created-date')]: row.createdTime, [this.translate.instant('alarm-activity.created-date')]: row.createdTime,
[this.translate.instant('alarm-activity.edited-date')]: row.editedTime, [this.translate.instant('alarm-activity.edited-date')]: row.editedTime,
[this.translate.instant('alarm-activity.text')]: row.commentText [this.translate.instant('alarm-activity.text')]: row.commentText
} };
dataToExport.push(exportRow) dataToExport.push(exportRow);
} }
return dataToExport; 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 { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { import {
AlarmAssignee,
AlarmInfo, AlarmInfo,
AlarmQueryV2, AlarmQueryV2,
AlarmSearchStatus, AlarmSearchStatus,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
AlarmsMode, AlarmsMode,
AlarmStatus, alarmStatusTranslations,
alarmStatusTranslations getUserDisplayName,
getUserInitials
} from '@app/shared/models/alarm.models'; } from '@app/shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
@ -60,7 +62,7 @@ import {
AlarmAssigneePanelData AlarmAssigneePanelData
} from '@home/components/alarm/alarm-assignee-panel.component'; } from '@home/components/alarm/alarm-assignee-panel.component';
import { ComponentPortal } from '@angular/cdk/portal'; 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 { UtilsService } from '@core/services/utils.service';
import { AlarmFilterConfig } from '@shared/models/query/query.models'; import { AlarmFilterConfig } from '@shared/models/query/query.models';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
@ -129,22 +131,15 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
}))); })));
this.columns.push( this.columns.push(
new EntityTableColumn<AlarmInfo>('assignee', 'alarm.assignee', '240px', new EntityTableColumn<AlarmInfo>('assignee', 'alarm.assignee', '240px',
(entity) => { (entity) => this.getAssigneeTemplate(entity), () => ({}), false, () => ({}), () => undefined, false,
return this.getAssigneeTemplate(entity)
},
() => ({}),
false,
() => ({}),
(entity) => undefined,
false,
{ {
icon: 'keyboard_arrow_down', icon: 'keyboard_arrow_down',
type: CellActionDescriptorType.DEFAULT, type: CellActionDescriptorType.DEFAULT,
isEnabled: (entity) => true, isEnabled: () => true,
name: this.translate.instant('alarm.assign'), name: this.translate.instant('alarm.assign'),
onAction: ($event, entity) => this.openAlarmAssigneePanel($event, entity) onAction: ($event, entity) => this.openAlarmAssigneePanel($event, entity)
}) })
) );
this.columns.push( this.columns.push(
new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%', new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%',
(entity) => this.translate.instant(alarmStatusTranslations.get(entity.status)))); (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status))));
@ -177,7 +172,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
isEnabled: true, isEnabled: true,
onAction: ($event, entities) => this.deleteAlarms($event, entities) onAction: ($event, entities) => this.deleteAlarms($event, entities)
} }
) );
} }
fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> { fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> {
@ -216,61 +211,32 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
} }
getAssigneeTemplate(entity: AlarmInfo): string { getAssigneeTemplate(entity: AlarmInfo): string {
return ` const hasAssigneeId = isDefinedAndNotNull(entity.assigneeId);
<span class="assignee-cell"> let templateContent: string;
${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>`
}
getUserDisplayName(entity: AlarmInfo) { if (hasAssigneeId && ((isNotEmptyStr(entity.assignee?.firstName) || isNotEmptyStr(entity.assignee?.lastName)) ||
let displayName = ''; isNotEmptyStr(entity.assignee?.email))) {
if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) || templateContent = `
(entity.assignee.lastName && entity.assignee.lastName.length > 0)) { <span class="assigned-container">
if (entity.assignee.firstName) { <span class="user-avatar" style="background-color: ${this.getAvatarBgColor(entity.assignee)}">
displayName += entity.assignee.firstName; ${getUserInitials(entity.assignee)}
} </span>
if (entity.assignee.lastName) { <span class="user-display-name">${getUserDisplayName(entity.assignee)}</span>
if (displayName.length > 0) { </span>`;
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);
}
} else { } 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) { getAvatarBgColor(alarmAssignee: AlarmAssignee) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); return this.utilsService.stringToHslColor(getUserDisplayName(alarmAssignee), 40, 60);
} }
openAlarmAssigneePanel($event: Event, entity: AlarmInfo) { openAlarmAssigneePanel($event: Event, entity: AlarmInfo) {
@ -312,7 +278,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.viewContainerRef, injector)); this.viewContainerRef, injector));
componentRef.onDestroy(() => { componentRef.onDestroy(() => {
if (componentRef.instance.reassigned) { 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() { ngOnInit() {
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); 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> <mat-icon>edit</mat-icon>{{'dashboard.edit-mode' | translate}}</button>
</ng-container> </ng-container>
<ng-container *ngIf="isEdit"> <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}}" matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>done</mat-icon></button> <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> <mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button>
<button fxHide.gt-md mat-icon-button (click)="toggleDashboardEditMode()" <button fxHide.gt-md mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'action.cancel' | translate}}" matTooltip="{{'action.cancel' | translate}}"
[disabled]="isLoading$ | async"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>close</mat-icon></button> <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-icon>close</mat-icon>{{ 'action.cancel' | translate }}</button>
<mat-divider class="tb-toolbar-divider" vertical></mat-divider> <mat-divider class="tb-toolbar-divider" vertical></mat-divider>
</ng-container> </ng-container>
@ -352,6 +353,7 @@
mat-mini-fab mat-mini-fab
class="tb-btn-header tb-btn-edit" class="tb-btn-header tb-btn-edit"
(click)="saveDashboard()" (click)="saveDashboard()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}" matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>done</mat-icon></button> <mat-icon>done</mat-icon></button>
@ -360,6 +362,7 @@
mat-mini-fab mat-mini-fab
class="tb-btn-header tb-btn-edit" class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()" (click)="toggleDashboardEditMode()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.cancel' | translate}}" matTooltip="{{'action.cancel' | translate}}"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>close</mat-icon></button> <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; LayoutType = LayoutType;
private destroyed = false;
private forcePristine = false; private forcePristine = false;
get isDirty(): boolean { get isDirty(): boolean {
@ -469,7 +471,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
ngAfterViewInit() { ngAfterViewInit() {
this.dashboardResize$ = new ResizeObserver(() => { this.dashboardResize$ = new ResizeObserver(() => {
this.updateLayoutSizes(); this.ngZone.run(() => {
this.updateLayoutSizes();
});
}); });
this.dashboardResize$.observe(this.dashboardContainer.nativeElement); this.dashboardResize$.observe(this.dashboardContainer.nativeElement);
if (!this.widgetEditMode && !this.readonly && this.dashboardUtils.isEmptyDashboard(this.dashboard)) { if (!this.widgetEditMode && !this.readonly && this.dashboardUtils.isEmptyDashboard(this.dashboard)) {
@ -594,6 +598,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed = true;
this.cleanupDashboardCss(); this.cleanupDashboardCss();
if (this.isMobileApp && this.syncStateWithQueryParam) { if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.unregisterToggleLayoutFunction(); this.mobileService.unregisterToggleLayoutFunction();
@ -1106,16 +1111,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
public openDashboardState(state: string, openRightLayout?: boolean) { public openDashboardState(state: string, openRightLayout?: boolean) {
const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); if (!this.destroyed) {
if (layoutsData) { const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
this.dashboardCtx.state = state; if (layoutsData) {
this.dashboardCtx.aliasController.dashboardStateChanged(); this.dashboardCtx.state = state;
this.isRightLayoutOpened = openRightLayout ? true : false; this.dashboardCtx.aliasController.dashboardStateChanged();
this.updateLayouts(layoutsData); 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) { 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, defaultItemCols: 8,
defaultItemRows: 6, defaultItemRows: 6,
displayGrid: this.displayGrid, displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50}, resizable: {
draggable: {enabled: this.isEdit && !this.isEditingWidget}, 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(), itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (_, itemComponent) => { itemInitCallback: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
@ -354,7 +361,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.gridsterResize$ = new ResizeObserver(() => { this.gridsterResize$ = new ResizeObserver(() => {
this.onGridsterParentResize(); this.ngZone.run(() => {
this.onGridsterParentResize();
});
}); });
this.gridsterResize$.observe(this.gridster.el.parentElement); 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, ComponentFactoryResolver,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, Input, NgZone,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
@ -142,7 +142,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
private router: Router, private router: Router,
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private elementRef: ElementRef, private elementRef: ElementRef,
private fb: FormBuilder) { private fb: FormBuilder,
private zone: NgZone) {
super(store); super(store);
} }
@ -157,11 +158,13 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
}); });
} }
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); this.widgetResize$.observe(this.elementRef.nativeElement);
} }
@ -432,8 +435,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
} }
private getTimePageLinkInterval(): {startTime?: number, endTime?: number} { private getTimePageLinkInterval(): {startTime?: number; endTime?: number} {
const interval: {startTime?: number, endTime?: number} = {}; const interval: {startTime?: number; endTime?: number} = {};
switch (this.timewindow.history.historyType) { switch (this.timewindow.history.historyType) {
case HistoryWindowType.LAST_INTERVAL: case HistoryWindowType.LAST_INTERVAL:
const currentTime = Date.now(); const currentTime = Date.now();

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

@ -20,7 +20,7 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Input, Input, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild ViewChild
@ -121,7 +121,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
private dialogService: DialogService, private dialogService: DialogService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private elementRef: ElementRef, private elementRef: ElementRef,
private fb: FormBuilder) { private fb: FormBuilder,
private zone: NgZone) {
super(store); super(store);
this.dirtyValue = !this.activeValue; this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC };
@ -133,11 +134,13 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
ngOnInit() { ngOnInit() {
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); 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, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, Input, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
@ -141,7 +141,8 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private elementRef: ElementRef, private elementRef: ElementRef,
private fb: FormBuilder) { private fb: FormBuilder,
private zone: NgZone) {
super(store); super(store);
this.dirtyValue = !this.activeValue; this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'timestamp', direction: Direction.DESC }; const sortOrder: SortOrder = { property: 'timestamp', direction: Direction.DESC };
@ -151,11 +152,13 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
ngOnInit() { ngOnInit() {
this.componentResize$ = new ResizeObserver(() => { this.componentResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.componentResize$.observe(this.elementRef.nativeElement); this.componentResize$.observe(this.elementRef.nativeElement);
this.isReadOnly = this.adminService.getRepositorySettingsInfo().pipe(map(settings => settings.readOnly)); 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, Component,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild ViewChild
@ -105,7 +105,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
private dialog: MatDialog, private dialog: MatDialog,
private dialogs: DialogService, private dialogs: DialogService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private elementRef: ElementRef) { private elementRef: ElementRef,
private zone: NgZone) {
super(store); super(store);
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
@ -115,11 +116,13 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
ngOnInit(): void { ngOnInit(): void {
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); 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> <span [innerHTML]="cellContent(alarm, column, row)"></span>
<ng-container *ngIf="column.entityKey.key === 'assignee'"> <ng-container *ngIf="column.entityKey.key === 'assignee'">
<span class="assignee-cell" fxLayout="row" fxLayoutAlign="start center"> <span class="assignee-cell" fxLayout="row" fxLayoutAlign="start center">
<span *ngIf="alarm.assigneeId" class="assigned-container"> <span *ngIf="alarm.assigneeId && checkAssigneeHasName(alarm.assignee); else unassigned"
<span class="user-avatar" [style.backgroundColor]="getAvatarBgColor(alarm)"> class="assigned-container">
{{ getUserInitials(alarm) }} <span class="user-avatar" [style.backgroundColor]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }}
</span> </span>
<span [matTooltip]="getUserDisplayName(alarm)" <span [matTooltip]="getUserDisplayName(alarm.assignee)" matTooltipPosition="above">
matTooltipPosition="above"> {{ getUserDisplayName(alarm.assignee) }}
{{ getUserDisplayName(alarm) }}
</span> </span>
</span> </span>
<span *ngIf="!alarm.assigneeId" class="unassigned-container"> <ng-template #unassigned>
<mat-icon class="material-icons unassigned-icon">account_circle</mat-icon> <span class="unassigned-container">
<span matTooltip="{{ 'alarm.unassigned' | translate }}" <mat-icon class="material-icons unassigned-icon">{{ alarm.assigneeId ? 'no_accounts' : 'account_circle' }}</mat-icon>
matTooltipPosition="above" <span matTooltip="{{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }}"
style="vertical-align: middle" matTooltipPosition="above" style="vertical-align: middle">
translate> {{ (alarm.assigneeId ? 'alarm.user-deleted' : 'alarm.unassigned') | translate }}
alarm.unassigned </span>
</span> </span>
</span> </ng-template>
<button *ngIf="allowAssign" <button *ngIf="allowAssign"
mat-icon-button [disabled]="isLoading$ | async" mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'alarm.assign' | translate }}" 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 { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; 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 cssjs from '@core/css/css';
import { sortItems } from '@shared/models/page/page-link'; import { sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
@ -83,7 +92,16 @@ import {
DisplayColumnsPanelComponent, DisplayColumnsPanelComponent,
DisplayColumnsPanelData DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component'; } 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 { DatePipe } from '@angular/common';
import { import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
@ -273,11 +291,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
() => this.pageLink.page = 0 () => this.pageLink.page = 0
); );
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.ngZone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); this.widgetResize$.observe(this.elementRef.nativeElement);
} }
@ -414,7 +434,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (alarmField && alarmField.time) { if (alarmField && alarmField.time) {
keySettings.columnWidth = '120px'; keySettings.columnWidth = '120px';
} }
if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) { if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) {
keySettings.columnWidth = '120px'; keySettings.columnWidth = '120px';
} }
} }
@ -1097,43 +1117,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.rowStyleCache.length = 0; this.rowStyleCache.length = 0;
} }
getUserDisplayName(entity: AlarmInfo) { checkAssigneeHasName(alarmAssignee: AlarmAssignee): boolean {
let displayName = ''; return (isNotEmptyStr(alarmAssignee?.firstName) || isNotEmptyStr(alarmAssignee?.lastName)) ||
if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) || isNotEmptyStr(alarmAssignee?.email);
(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 { getUserDisplayName(alarmAssignee: AlarmAssignee) {
let initials = ''; return getUserDisplayName(alarmAssignee);
if (entity.assignee.firstName && entity.assignee.firstName.length || }
entity.assignee.lastName && entity.assignee.lastName.length) {
if (entity.assignee.firstName) { getUserInitials(alarmAssignee: AlarmAssignee): string {
initials += entity.assignee.firstName.charAt(0); return getUserInitials(alarmAssignee);
}
if (entity.assignee.lastName) {
initials += entity.assignee.lastName.charAt(0);
}
} else {
initials += entity.assignee.email.charAt(0);
}
return initials.toUpperCase();
} }
getAvatarBgColor(entity: AlarmInfo) { getAvatarBgColor(alarmAssignee: AlarmAssignee) {
return this.utils.stringToHslColor(this.getUserDisplayName(entity), 40, 60); return this.utils.stringToHslColor(this.getUserDisplayName(alarmAssignee), 40, 60);
} }
openAlarmAssigneePanel($event: Event, entity: AlarmInfo) { 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(); this.ctx.updateWidgetParams();
if (this.displayPagination) { if (this.displayPagination) {
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.ngZone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); 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> { getDowngradedVersion(): GatewayConnector<MQTTLegacyBasicConfig> {
const { requestsMapping, mapping, ...restConfig } = this.connector.configurationJson as MQTTBasicConfig_v3_5_2; const { requestsMapping, mapping, ...restConfig } = this.connector.configurationJson as MQTTBasicConfig_v3_5_2;
const updatedRequestsMapping = const updatedRequestsMapping = requestsMapping
MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>); ? MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>) : {};
const updatedMapping = MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping); const updatedMapping = MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping);
return { 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 { takeUntil } from 'rxjs/operators';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { GatewayConfigValue } from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models';
@Component({ @Component({
selector: 'tb-gateway-advanced-configuration', selector: 'tb-gateway-advanced-configuration',
@ -83,8 +84,8 @@ export class GatewayAdvancedConfigurationComponent implements OnDestroy, Control
this.onTouched = fn; this.onTouched = fn;
} }
writeValue(basicConfig: unknown): void { writeValue(advancedConfig: GatewayConfigValue): void {
this.advancedFormControl.reset(basicConfig, {emitEvent: false}); this.advancedFormControl.reset(advancedConfig, {emitEvent: false});
} }
validate(): ValidationErrors | null { 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> </mat-form-field>
</section> </section>
<button mat-icon-button (click)="removeCommandControl($index, $event)" <button mat-icon-button (click)="removeCommandControl($index, $event)"
class="tb-box-button"
[disabled]="!basicFormGroup.get('thingsboard.remoteConfiguration').value" [disabled]="!basicFormGroup.get('thingsboard.remoteConfiguration').value"
matTooltip="{{ 'gateway.statistics.remove' | translate }}" matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</div> </div>
<button mat-stroked-button color="primary" <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, FormGroup,
} from '@angular/forms'; } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id'; 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 { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { DeviceService } from '@core/http/device.service'; import { DeviceService } from '@core/http/device.service';
@ -40,7 +40,9 @@ import {
GatewayConfigSecurity, GatewayConfigSecurity,
GatewayConfigValue, GatewayConfigValue,
GatewayGeneralConfig, GatewayGeneralConfig,
GatewayGRPCConfig,
GatewayLogsConfig, GatewayLogsConfig,
GatewayStorageConfig,
LocalLogs, LocalLogs,
LogAttribute, LogAttribute,
LogConfig, LogConfig,
@ -113,7 +115,7 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
this.gatewayConfigGroup.get('basicConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { this.gatewayConfigGroup.get('basicConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
const advancedControl = this.gatewayConfigGroup.get('advancedConfig'); 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}); 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 => { this.gatewayConfigGroup.get('advancedConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
const basicControl = this.gatewayConfigGroup.get('basicConfig'); 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}); basicControl.patchValue(value, {emitEvent: false});
} }
}); });
@ -320,10 +322,10 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
private updateConfigs(attributes: AttributeData[]): void { private updateConfigs(attributes: AttributeData[]): void {
const formValue: GatewayConfigValue = { const formValue: GatewayConfigValue = {
thingsboard: null, thingsboard: {} as GatewayGeneralConfig,
grpc: null, grpc: {} as GatewayGRPCConfig,
logs: null, logs: {} as GatewayLogsConfig,
storage: null, storage: {} as GatewayStorageConfig,
mode: ConfigurationModes.BASIC 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 { export interface GatewayConfigValue {
mode: ConfigurationModes; mode: ConfigurationModes;
thingsboard: GatewayGeneralConfig; thingsboard: GatewayGeneralConfig;
storage: { storage: GatewayStorageConfig;
type: StorageTypes; grpc: GatewayGRPCConfig;
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;
};
connectors?: GatewayConnector[]; connectors?: GatewayConnector[];
logs: GatewayLogsConfig; 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 { export interface GatewayGeneralConfig {
host: string; host: string;
port: number; 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 { private getMappingValue(value: ConnectorMapping): MappingValue {
switch (this.mappingType) { switch (this.mappingType) {
case MappingType.DATA: case MappingType.DATA:
const converterType = ConvertorTypeTranslationsMap.get((value as ConverterConnectorMapping).converter?.type);
return { return {
topicFilter: (value as ConverterConnectorMapping).topicFilter, topicFilter: (value as ConverterConnectorMapping).topicFilter,
QoS: (value as ConverterConnectorMapping).subscriptionQos, 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: case MappingType.REQUESTS:
let details: string; 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> </mat-form-field>
</div> </div>
</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 *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="fixed-title-width tb-required" translate>gateway.value</div>
<div class="tb-flex no-gap"> <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 { import {
AbstractControl, AbstractControl,
FormArray, FormArray,
FormControl,
FormGroup, FormGroup,
UntypedFormArray, UntypedFormArray,
UntypedFormBuilder, UntypedFormBuilder,
@ -27,12 +28,15 @@ import {
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { import {
ModbusDataType, ModbusDataType,
ModbusEditableDataTypes, ModbusEditableDataTypes, ModbusFormValue,
ModbusFunctionCodeTranslationsMap, ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType, ModbusObjectCountByDataType,
ModbusValue, ModbusValue,
ModbusValueKey, ModbusValueKey,
ModifierType,
ModifierTypesMap,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
nonZeroFloat,
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
@ -75,13 +79,18 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
keysListFormArray: FormArray<UntypedFormGroup>; keysListFormArray: FormArray<UntypedFormGroup>;
modbusDataTypes = Object.values(ModbusDataType); modbusDataTypes = Object.values(ModbusDataType);
modifierTypes: ModifierType[] = Object.values(ModifierType);
withFunctionCode = true; withFunctionCode = true;
withReportStrategy = true; withReportStrategy = true;
enableModifiersControlMap = new Map<string, FormControl<boolean>>();
showModifiersMap = new Map<string, boolean>();
functionCodesMap = new Map(); functionCodesMap = new Map();
defaultFunctionCodes = []; defaultFunctionCodes = [];
readonly ModbusEditableDataTypes = ModbusEditableDataTypes; readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly ModifierTypesMap = ModifierTypesMap;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -111,6 +120,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
} }
addKey(): void { addKey(): void {
const id = generateSecret(5);
const dataKeyFormGroup = this.fb.group({ const dataKeyFormGroup = this.fb.group({
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{value: '', disabled: !this.isMaster}, [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]], objectsCount: [1, [Validators.required]],
functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]], functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]],
reportStrategy: [{ value: null, disabled: !this.withReportStrategy }], 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.observeKeyDataType(dataKeyFormGroup);
this.observeEnableModifier(dataKeyFormGroup);
this.keysListFormArray.push(dataKeyFormGroup); this.keysListFormArray.push(dataKeyFormGroup);
} }
@ -143,9 +158,11 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
} }
private getFormValue(): ModbusValue[] { private getFormValue(): ModbusValue[] {
return this.withReportStrategy return this.mapKeysWithModifier(
this.withReportStrategy
? this.cleanUpEmptyStrategies(this.keysListFormArray.value) ? this.cleanUpEmptyStrategies(this.keysListFormArray.value)
: this.keysListFormArray.value; : this.keysListFormArray.value
);
} }
private cleanUpEmptyStrategies(values: ModbusValue[]): ModbusValue[] { 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 { private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = []; const keysControlGroups: Array<AbstractControl> = [];
@ -162,6 +189,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
values.forEach(value => { values.forEach(value => {
const dataKeyFormGroup = this.createDataKeyFormGroup(value); const dataKeyFormGroup = this.createDataKeyFormGroup(value);
this.observeKeyDataType(dataKeyFormGroup); this.observeKeyDataType(dataKeyFormGroup);
this.observeEnableModifier(dataKeyFormGroup);
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type)); this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type));
keysControlGroups.push(dataKeyFormGroup); keysControlGroups.push(dataKeyFormGroup);
@ -172,7 +200,12 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
} }
private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup { 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({ return this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -181,20 +214,55 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
address: [address, [Validators.required]], address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]], objectsCount: [objectsCount, [Validators.required]],
functionCode: [{ value: functionCode, disabled: !this.withFunctionCode }, [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 }], 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 { private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.ModbusEditableDataTypes.includes(dataType)) { if (!this.ModbusEditableDataTypes.includes(dataType)) {
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); 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); 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 { private updateFunctionCodes(keyFormGroup: FormGroup, dataType: ModbusDataType): void {
const functionCodes = this.getFunctionCodes(dataType); const functionCodes = this.getFunctionCodes(dataType);
this.functionCodesMap.set(keyFormGroup.get('id').value, functionCodes); 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> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <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"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="security"> <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 { 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 { const {
name: connectorName, name: connectorName,
id: connectorId, id: connectorId,
enableRemoteLogging: connectorEnableRemoteLogging, enableRemoteLogging: connectorEnableRemoteLogging,
logLevel: connectorLogLevel, logLevel: connectorLogLevel,
reportStrategy: connectorReportStrategy, reportStrategy: connectorReportStrategy,
configVersion: connectorConfigVersion,
...connectorConfig ...connectorConfig
} = connectorDataConfigJson; } = 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 noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/;
export const integerRegex = /^[-+]?\d+$/; export const integerRegex = /^[-+]?\d+$/;
export const nonZeroFloat = /^-?(?!0(\.0+)?$)\d+(\.\d+)?$/;
export enum StorageTypes { export enum StorageTypes {
MEMORY = 'memory', MEMORY = 'memory',
@ -208,6 +209,7 @@ export interface ConnectorBaseInfo {
id: string; id: string;
enableRemoteLogging: boolean; enableRemoteLogging: boolean;
logLevel: GatewayLogLevel; logLevel: GatewayLogLevel;
configVersion: string | number;
reportStrategy?: ReportStrategyConfig; reportStrategy?: ReportStrategyConfig;
} }
@ -913,6 +915,30 @@ export enum MappingValueType {
BOOLEAN = 'boolean' 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>( export const mappingValueTypesMap = new Map<MappingValueType, ValueTypeData>(
[ [
[ [
@ -1162,6 +1188,13 @@ export interface ModbusValue {
address: number; address: number;
value?: string; value?: string;
reportStrategy?: ReportStrategyConfig; reportStrategy?: ReportStrategyConfig;
multiplier?: number;
divider?: number;
}
export interface ModbusFormValue extends ModbusValue {
modifierType?: ModifierType;
modifierValue?: string;
} }
export interface ModbusSecurity { 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 { export class OpcVersionMappingUtil {
static mapServerToUpgradedVersion(server: LegacyServerConfig): ServerConfig { static mapServerToUpgradedVersion(server: LegacyServerConfig): ServerConfig {
const { mapping, disableSubscriptions, ...restServer } = server; const { mapping, disableSubscriptions, pollPeriodInMillis, ...restServer } = server;
return { return {
...restServer, ...restServer,
pollPeriodInMillis: pollPeriodInMillis ?? 5000,
enableSubscriptions: !disableSubscriptions, 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. /// 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 { PageComponent } from '@shared/components/page.component';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -81,7 +81,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
private utilsService: UtilsService, private utilsService: UtilsService,
private elementRef: ElementRef, private elementRef: ElementRef,
private imagePipe: ImagePipe, private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,) { private sanitizer: DomSanitizer,
private zone: NgZone) {
super(store); super(store);
} }
@ -101,11 +102,13 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI
} }
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250; this.zone.run(() => {
if (showHideBadgeContainer !== this.showBadgeContainer) { const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250;
this.showBadgeContainer = showHideBadgeContainer; if (showHideBadgeContainer !== this.showBadgeContainer) {
this.cd.markForCheck(); this.showBadgeContainer = showHideBadgeContainer;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); 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.buildForm();
this.ctx.updateWidgetParams(); this.ctx.updateWidgetParams();
this.formResize$ = new ResizeObserver(() => { this.formResize$ = new ResizeObserver(() => {
this.resize(); this.ngZone.run(() => {
this.resize();
});
}); });
this.formResize$.observe(this.formContainerRef.nativeElement); 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 { import {
AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Injector, Injector,
Input, Input, NgZone, OnDestroy,
OnInit, OnInit,
StaticProvider, StaticProvider,
ViewChild, ViewChild,
@ -100,7 +101,7 @@ interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDes
styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss'] 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() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -144,7 +145,8 @@ export class PersistentTableComponent extends PageComponent implements OnInit {
private dialogService: DialogService, private dialogService: DialogService,
private deviceService: DeviceService, private deviceService: DeviceService,
private dialog: MatDialog, private dialog: MatDialog,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef,
private zone: NgZone) {
super(store); super(store);
} }
@ -158,11 +160,13 @@ export class PersistentTableComponent extends PageComponent implements OnInit {
this.ctx.updateWidgetParams(); this.ctx.updateWidgetParams();
if (this.displayPagination) { if (this.displayPagination) {
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); 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, deepClone,
formatValue, formatValue,
guid, guid,
isDefinedAndNotNull, isFirefox, isDefinedAndNotNull,
isNumeric, isSafari, isFirefox,
isNumeric,
isSafari,
isUndefined, isUndefined,
isUndefinedOrNull, isUndefinedOrNull,
mergeDeep, mergeDeep,
@ -1216,8 +1218,8 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
return this; return this;
} }
public ease(easing: string): this { public ease(easing: string | EasingLiteral): this {
this._easing = easing; this._easing = this.easingLiteralToCssEasing(easing);
this.updateAnimationStyle('animation-timing-function', this._easing); this.updateAnimationStyle('animation-timing-function', this._easing);
return this; return this;
} }
@ -1551,10 +1553,10 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
transform = deepClone(transform); transform = deepClone(transform);
Object.assign(transform, inputTransform); Object.assign(transform, inputTransform);
} }
return { return {
'transform-origin': `${transform.originX}px ${transform.originY}px`, 'transform-origin': `${transform.originX}px ${transform.originY}px`,
transform: `translate(${transform.translateX}px, ${transform.translateY}px) ` + transform: `translate(${transform.translateX}px, ${transform.translateY}px) ` +
`skewX(${transform.b}deg) skewY(${transform.c}deg) ` +
`scale(${transform.scaleX}, ${transform.scaleY}) ` + `scale(${transform.scaleX}, ${transform.scaleY}) ` +
`rotate(${transform.rotate}deg)`}; `rotate(${transform.rotate}deg)`};
} }
@ -1563,6 +1565,22 @@ class CssScadaSymbolAnimation implements ScadaSymbolAnimation {
const factor = Math.pow(10, digits); const factor = Math.pow(10, digits);
return Math.round((num + Number.EPSILON) * factor) / factor; 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 { 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, Component,
ElementRef, ElementRef,
Injector, Injector,
Input, Input, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
@ -224,7 +224,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private domSanitizer: DomSanitizer, private domSanitizer: DomSanitizer,
private datePipe: DatePipe, private datePipe: DatePipe,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private fb: FormBuilder) { private fb: FormBuilder,
private zone: NgZone) {
super(store); super(store);
} }
@ -249,11 +250,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
); );
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); 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, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ComponentRef,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Input, OnChanges, Input,
OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
Renderer2, SimpleChanges, Renderer2,
ViewChild, ViewContainerRef, SimpleChanges,
ViewChild,
ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -38,9 +42,9 @@ import { SafeStyle } from '@angular/platform-browser';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2'; import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { from } from 'rxjs'; import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
export enum WidgetComponentActionType { export enum WidgetComponentActionType {
MOUSE_DOWN, MOUSE_DOWN,
@ -173,7 +177,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
if (this.cssClass) { if (this.cssClass) {
this.utils.clearCssElement(this.renderer, this.cssClass); this.utils.clearCssElement(this.renderer, this.cssClass);
} }
if (this.editWidgetActionsTooltip) { if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) {
this.editWidgetActionsTooltip.destroy(); this.editWidgetActionsTooltip.destroy();
} }
} }
@ -255,6 +259,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
private initEditWidgetActionTooltip(parent: HTMLElement) { private initEditWidgetActionTooltip(parent: HTMLElement) {
let componentRef: ComponentRef<EditWidgetActionsTooltipComponent>;
from(import('tooltipster')).subscribe(() => { from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({ $(this.gridsterItem.el).tooltipster({
parent: $(parent), parent: $(parent),
@ -305,7 +310,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
}); });
this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance'); this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance');
const componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent); componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent);
componentRef.instance.container = this; componentRef.instance.container = this;
componentRef.instance.viewInited.subscribe(() => { componentRef.instance.viewInited.subscribe(() => {
if (this.editWidgetActionsTooltip.status().open) { 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 { } 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); return Math.floor(res);
} }
set x(x: number) { set x(_: number) {}
if (!this.dashboard.isMobileSize && this.dashboard.isEdit) {
if (this.widgetLayout) {
this.widgetLayout.col = x;
} else {
this.widget.col = x;
}
}
}
@enumerable(true) @enumerable(true)
get y(): number { get y(): number {
@ -727,15 +719,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
return Math.floor(res); return Math.floor(res);
} }
set y(y: number) { set y(_: number) {}
if (!this.dashboard.isMobileSize && this.dashboard.isEdit) {
if (this.widgetLayout) {
this.widgetLayout.row = y;
} else {
this.widget.row = y;
}
}
}
get preserveAspectRatio(): boolean { get preserveAspectRatio(): boolean {
if (!this.dashboard.isMobileSize && this.widgetLayout) { if (!this.dashboard.isMobileSize && this.widgetLayout) {
@ -833,4 +817,14 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
} }
return order; 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. /// 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 { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog'; 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 { getProviderHelpLink, OAuth2Client } from '@shared/models/oauth2.models';
import { OAuth2Service } from '@core/http/oauth2.service'; import { OAuth2Service } from '@core/http/oauth2.service';
import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; 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}], providers: [{provide: ErrorStateMatcher, useExisting: ClientDialogComponent}],
styleUrls: [] styleUrls: []
}) })
export class ClientDialogComponent extends DialogComponent<ClientDialogComponent, OAuth2Client> implements OnDestroy { export class ClientDialogComponent extends DialogComponent<ClientDialogComponent, OAuth2Client> implements OnDestroy, AfterViewInit {
submitted = false; submitted = false;
@ -41,7 +41,6 @@ export class ClientDialogComponent extends DialogComponent<ClientDialogComponent
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
protected dialogRef: MatDialogRef<ClientDialogComponent, OAuth2Client>, protected dialogRef: MatDialogRef<ClientDialogComponent, OAuth2Client>,
private fb: FormBuilder,
private oauth2Service: OAuth2Service, private oauth2Service: OAuth2Service,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { @SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store, router, dialogRef); 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. /// 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 { EntityComponent } from '@home/components/entity/entity.component';
import { import {
ClientAuthenticationMethod, ClientAuthenticationMethod,
@ -54,7 +54,7 @@ import { coerceBoolean } from '@app/shared/decorators/coercion';
templateUrl: './client.component.html', templateUrl: './client.component.html',
styleUrls: ['./client.component.scss'] styleUrls: ['./client.component.scss']
}) })
export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAuth2ClientInfo> { export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAuth2ClientInfo> implements OnDestroy {
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -109,7 +109,8 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
protected translate: TranslateService, protected translate: TranslateService,
private oauth2Service: OAuth2Service, private oauth2Service: OAuth2Service,
@Optional() @Inject('entity') protected entityValue: OAuth2Client, @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, protected cd: ChangeDetectorRef,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder) {
super(store, fb, entityValue, entitiesTableConfigValue, cd); super(store, fb, entityValue, entitiesTableConfigValue, cd);
@ -185,13 +186,13 @@ export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAu
if (entity.scope.length === scopeControls.length) { if (entity.scope.length === scopeControls.length) {
scopeControls.patchValue(entity.scope, {emitEvent: false}); scopeControls.patchValue(entity.scope, {emitEvent: false});
} else { } else {
const scopeControls: Array<AbstractControl> = []; const newScopeControls: Array<AbstractControl> = [];
if (entity.scope) { if (entity.scope) {
for (const scope of 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, OAuth2ClientInfo,
platformTypeTranslations platformTypeTranslations
} from '@shared/models/oauth2.models'; } from '@shared/models/oauth2.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
@ -40,11 +39,11 @@ import { PageLink } from '@shared/models/page/page-link';
@Injectable() @Injectable()
export class ClientsTableConfigResolver implements Resolve<EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>> { 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, constructor(private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private utilsService: UtilsService,
private oauth2Service: OAuth2Service) { private oauth2Service: OAuth2Service) {
this.config.tableTitle = this.translate.instant('admin.oauth2.clients'); this.config.tableTitle = this.translate.instant('admin.oauth2.clients');
this.config.selectionEnabled = false; 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.entityTranslations = entityTypeTranslations.get(EntityType.OAUTH2_CLIENT);
this.config.entityResources = { this.config.entityResources = {
helpLinkId: null, helpLinkId: null,
helpLinkIdForEntity(entity: OAuth2Client): string { helpLinkIdForEntity: (entity: OAuth2Client) => getProviderHelpLink(entity.additionalInfo.providerName)
return getProviderHelpLink(entity.additionalInfo.providerName);
}
}; };
this.config.entityComponent = ClientComponent; this.config.entityComponent = ClientComponent;
this.config.headerComponent = ClientTableHeaderComponent; 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 DateEntityTableColumn<OAuth2ClientInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),
new EntityTableColumn<OAuth2ClientInfo>('title', 'admin.oauth2.title', '350px'), new EntityTableColumn<OAuth2ClientInfo>('title', 'admin.oauth2.title', '350px'),
new EntityTableColumn<OAuth2ClientInfo>('platforms', 'admin.oauth2.allowed-platforms', '100%', new EntityTableColumn<OAuth2ClientInfo>('platforms', 'admin.oauth2.allowed-platforms', '100%',
(clientInfo) => { (clientInfo) => clientInfo.platforms && clientInfo.platforms.length ?
return clientInfo.platforms && clientInfo.platforms.length ? clientInfo.platforms.map(platform => this.translate.instant(platformTypeTranslations.get(platform))).join(', ') :
clientInfo.platforms.map(platform => this.translate.instant(platformTypeTranslations.get(platform))).join(', ') : this.translate.instant('admin.oauth2.all-platforms'), () => ({}), false)
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.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.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-client-text');
this.config.entitiesFetchFunction = pageLink => this.oauth2Service.findTenantOAuth2ClientInfos(pageLink); this.config.entitiesFetchFunction = pageLink => this.oauth2Service.findTenantOAuth2ClientInfos(pageLink);
this.config.loadEntity = id => this.oauth2Service.getOAuth2ClientById(id.id); this.config.loadEntity = id => this.oauth2Service.getOAuth2ClientById(id.id);
this.config.saveEntity = client => { this.config.saveEntity = client => this.oauth2Service.saveOAuth2Client(client);
return this.oauth2Service.saveOAuth2Client(client);
}
this.config.deleteEntity = id => this.oauth2Service.deleteOauth2Client(id.id); 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 { isEqual } from '@core/utils';
import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component';
import { Direction } from '@app/shared/models/page/sort-order'; import { Direction } from '@app/shared/models/page/sort-order';
import { map } from 'rxjs'; import { map, Observable, of, mergeMap } from 'rxjs';
@Injectable() @Injectable()
export class DomainTableConfigResolver implements Resolve<EntityTableConfig<DomainInfo>> { 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.loadEntity = id => this.domainService.getDomainInfoById(id.id);
this.config.saveEntity = (domain, originalDomain) => { this.config.saveEntity = (domain, originalDomain) => {
const clientsIds = domain.oauth2ClientInfos as Array<string> || []; const clientsIds = domain.oauth2ClientInfos as Array<string> || [];
let clientsTask: Observable<void>;
if (domain.id && !isEqual(domain.oauth2ClientInfos?.sort(), if (domain.id && !isEqual(domain.oauth2ClientInfos?.sort(),
originalDomain.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).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; delete domain.oauth2ClientInfos;
return this.domainService.saveDomain(domain, domain.id ? [] : clientsIds).pipe( return clientsTask.pipe(
map(domain => { mergeMap(() => this.domainService.saveDomain(domain, domain.id ? [] : clientsIds)),
(domain as DomainInfo).oauth2ClientInfos = clientsIds; map(savedDomain => {
return domain; (savedDomain as DomainInfo).oauth2ClientInfos = clientsIds;
return savedDomain;
}) })
); );
} };
this.config.deleteEntity = id => this.domainService.deleteDomain(id.id); 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" <tb-entity-list [entityType]="entityType.OAUTH2_CLIENT" formControlName="oauth2ClientInfos"
labelText="{{ 'admin.oauth2.clients' | translate }}" labelText="{{ 'admin.oauth2.clients' | translate }}"
placeholderText="{{ 'admin.oauth2.add-client' | translate }}"> placeholderText="{{ 'admin.oauth2.add-client' | translate }}">
<button #createTargetButton <button mat-button color="primary" matSuffix
mat-button color="primary" matSuffix
[disabled]="!isEdit" [disabled]="!isEdit"
(click)="createClient($event, createTargetButton)"> (click)="createClient($event)">
<span style="white-space: nowrap">{{ 'admin.oauth2.create-new' | translate }}</span> <span style="white-space: nowrap">{{ 'admin.oauth2.create-new' | translate }}</span>
</button> </button>
</tb-entity-list> </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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { MatButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component';
import { EntityType } from '@shared/models/entity-type.models'; 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> { export class DomainComponent extends EntityComponent<DomainInfo> {
private loginProcessingUrl: string = ''; private loginProcessingUrl = '';
entityType = EntityType; entityType = EntityType;
@ -83,7 +82,7 @@ export class DomainComponent extends EntityComponent<DomainInfo> {
return domainName !== '' ? `${domainName}${this.loginProcessingUrl}` : ''; return domainName !== '' ? `${domainName}${this.loginProcessingUrl}` : '';
} }
createClient($event: Event, button: MatButton) { createClient($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
$event.preventDefault(); $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 EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; 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 { MobileAppService } from '@core/http/mobile-app.service';
import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; 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 { 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() @Injectable()
export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<MobileAppInfo>> { export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<MobileAppInfo>> {
@ -43,7 +42,6 @@ export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<M
constructor(private translate: TranslateService, constructor(private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private utilsService: UtilsService,
private mobileAppService: MobileAppService) { private mobileAppService: MobileAppService) {
this.config.tableTitle = this.translate.instant('admin.oauth2.mobile-apps'); this.config.tableTitle = this.translate.instant('admin.oauth2.mobile-apps');
this.config.selectionEnabled = false; 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'), name: this.translate.instant('admin.oauth2.copy-mobile-app-secret'),
icon: 'content_copy', icon: 'content_copy',
style: { style: {
'padding-top': '1px', padding: '4px',
'font-size': '16px', 'font-size': '16px',
color: 'rgba(0,0,0,.87)' 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.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id);
this.config.saveEntity = (mobileApp, originalMobileApp) => { this.config.saveEntity = (mobileApp, originalMobileApp) => {
const clientsIds = mobileApp.oauth2ClientInfos as Array<string> || []; const clientsIds = mobileApp.oauth2ClientInfos as Array<string> || [];
let clientsTask: Observable<void>;
if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(), if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos?.sort(),
originalMobileApp.oauth2ClientInfos?.map(info => info.id ? info.id.id : info).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; delete mobileApp.oauth2ClientInfos;
return this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds).pipe( return clientsTask.pipe(
map(mobileApp => { mergeMap(() => this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds)),
(mobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds; map(savedMobileApp => {
return mobileApp; (savedMobileApp as MobileAppInfo).oauth2ClientInfos = clientsIds;
return savedMobileApp;
}) })
); );
} };
this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); 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')"> <mat-error *ngIf="entityForm.get('pkgName').hasError('maxlength')">
{{ 'admin.oauth2.mobile-package-max-length' | translate }} {{ 'admin.oauth2.mobile-package-max-length' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="entityForm.get('pkgName').hasError('pattern')">
{{ 'admin.oauth2.mobile-package-spaces' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row tb-standard-fields no-border no-padding"> <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" <tb-entity-list [entityType]="entityType.OAUTH2_CLIENT" formControlName="oauth2ClientInfos"
labelText="{{ 'admin.oauth2.clients' | translate }}" labelText="{{ 'admin.oauth2.clients' | translate }}"
placeholderText="{{ 'admin.oauth2.add-client' | translate }}"> placeholderText="{{ 'admin.oauth2.add-client' | translate }}">
<button #createTargetButton <button mat-button color="primary" matSuffix
mat-button color="primary" matSuffix
[disabled]="!isEdit" [disabled]="!isEdit"
(click)="createClient($event, createTargetButton)"> (click)="createClient($event)">
<span style="white-space: nowrap">{{ 'admin.oauth2.create-new' | translate }}</span> <span style="white-space: nowrap">{{ 'admin.oauth2.create-new' | translate }}</span>
</button> </button>
</tb-entity-list> </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 { EntityComponent } from '@home/components/entity/entity.component';
import { MobileAppInfo } from '@shared/models/oauth2.models'; import { MobileAppInfo } from '@shared/models/oauth2.models';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { OAuth2Service } from '@core/http/oauth2.service';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
import { WINDOW } from '@core/services/window.service';
import { isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; import { isDefinedAndNotNull, randomAlphanumeric } from '@core/utils';
import { MatButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
@ -42,19 +39,18 @@ export class MobileAppComponent extends EntityComponent<MobileAppInfo> {
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected translate: TranslateService, protected translate: TranslateService,
private oauth2Service: OAuth2Service,
@Inject('entity') protected entityValue: MobileAppInfo, @Inject('entity') protected entityValue: MobileAppInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<MobileAppInfo>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<MobileAppInfo>,
protected cd: ChangeDetectorRef, protected cd: ChangeDetectorRef,
public fb: UntypedFormBuilder, public fb: UntypedFormBuilder,
@Inject(WINDOW) private window: Window,
private dialog: MatDialog) { private dialog: MatDialog) {
super(store, fb, entityValue, entitiesTableConfigValue, cd); super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
buildForm(entity: MobileAppInfo): UntypedFormGroup { buildForm(entity: MobileAppInfo): UntypedFormGroup {
return this.fb.group({ 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)), appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)),
[Validators.required, this.base64Format]], [Validators.required, this.base64Format]],
oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true, oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : true,
@ -68,10 +64,10 @@ export class MobileAppComponent extends EntityComponent<MobileAppInfo> {
appSecret: entity.appSecret, appSecret: entity.appSecret,
oauth2Enabled: entity.oauth2Enabled, oauth2Enabled: entity.oauth2Enabled,
oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info) oauth2ClientInfos: entity.oauth2ClientInfos?.map(info => info.id ? info.id.id : info)
}) });
} }
createClient($event: Event, button: MatButton) { createClient($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
$event.preventDefault(); $event.preventDefault();

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

@ -20,7 +20,7 @@ import {
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, Input, NgZone,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
@ -102,7 +102,8 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
this.updateEditorMode(value); this.updateEditorMode(value);
} }
constructor(private cd: ChangeDetectorRef) { constructor(private cd: ChangeDetectorRef,
private zone: NgZone) {
} }
ngOnInit(): void { ngOnInit(): void {
@ -128,7 +129,7 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
}; };
this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement, this.scadaSymbolEditObject = new ScadaSymbolEditObject(this.scadaSymbolShape.nativeElement,
this.tooltipsContainer.nativeElement, this.tooltipsContainer.nativeElement,
this.tooltipsContainerComponent.viewContainerRef, this.editObjectCallbacks, this.readonly); this.tooltipsContainerComponent.viewContainerRef, this.zone, this.editObjectCallbacks, this.readonly);
if (this.data) { if (this.data) {
this.updateContent(this.data.scadaSymbolContent); 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 { ImageResourceInfo } from '@shared/models/resource.models';
import * as svgjs from '@svgdotjs/svg.js'; import * as svgjs from '@svgdotjs/svg.js';
import { Box, Element, Rect, Style, SVG, Svg, Timeline } 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 { forkJoin, from } from 'rxjs';
import { import {
setupAddTagPanelTooltip, setupAddTagPanelTooltip,
@ -79,6 +79,7 @@ export class ScadaSymbolEditObject {
constructor(private rootElement: HTMLElement, constructor(private rootElement: HTMLElement,
public tooltipsContainer: HTMLElement, public tooltipsContainer: HTMLElement,
public viewContainerRef: ViewContainerRef, public viewContainerRef: ViewContainerRef,
private zone: NgZone,
private callbacks: ScadaSymbolEditObjectCallbacks, private callbacks: ScadaSymbolEditObjectCallbacks,
public readonly: boolean) { public readonly: boolean) {
this.shapeResize$ = new ResizeObserver(() => { this.shapeResize$ = new ResizeObserver(() => {
@ -193,7 +194,9 @@ export class ScadaSymbolEditObject {
from(import('tooltipster')), from(import('tooltipster')),
from(import('tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js')) from(import('tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js'))
]).subscribe(() => { ]).subscribe(() => {
this.setupElements(); this.zone.run(() => {
this.setupElements();
});
}); });
} }
@ -826,6 +829,7 @@ export class ScadaSymbolElement {
private innerAddTagTooltipPosition(_instance: ITooltipsterInstance, private innerAddTagTooltipPosition(_instance: ITooltipsterInstance,
_helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition { _helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition {
const distance = 10; const distance = 10;
const parentRect = this.tooltipContainer[0].getBoundingClientRect();
switch (position.side) { switch (position.side) {
case 'right': case 'right':
position.coord.top = this.tooltipMouseY - position.size.height / 2; position.coord.top = this.tooltipMouseY - position.size.height / 2;
@ -851,6 +855,14 @@ export class ScadaSymbolElement {
position.target = this.tooltipMouseX; position.target = this.tooltipMouseX;
break; 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; return position;
} }

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

@ -20,27 +20,30 @@
formControlName="country" formControlName="country"
(selectCountryCode)="changeCountry($event)"> (selectCountryCode)="changeCountry($event)">
</tb-country-autocomplete> </tb-country-autocomplete>
<div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px"> <div class="tb-form-row column-xs tb-standard-fields no-border no-padding">
<mat-form-field class="mat-block"> <mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label translate>contact.city</mat-label> <mat-label translate>contact.city</mat-label>
<input matInput formControlName="city"> <input matInput formControlName="city">
<mat-error *ngIf="parentForm.get('city').hasError('maxlength')"> <mat-error *ngIf="parentForm.get('city').hasError('maxlength')">
{{ 'contact.city-max-length' | translate }} {{ 'contact.city-max-length' | translate }}
</mat-error> </mat-error>
<mat-hint></mat-hint>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label translate>contact.state</mat-label> <mat-label translate>contact.state</mat-label>
<input matInput formControlName="state"> <input matInput formControlName="state">
<mat-error *ngIf="parentForm.get('state').hasError('maxlength')"> <mat-error *ngIf="parentForm.get('state').hasError('maxlength')">
{{ 'contact.state-max-length' | translate }} {{ 'contact.state-max-length' | translate }}
</mat-error> </mat-error>
<mat-hint></mat-hint>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label translate>contact.postal-code</mat-label> <mat-label translate>contact.postal-code</mat-label>
<input matInput formControlName="zip"> <input matInput formControlName="zip">
<mat-error *ngIf="parentForm.get('zip').hasError('pattern')"> <mat-error *ngIf="parentForm.get('zip').hasError('pattern')">
{{ 'contact.postal-code-invalid' | translate }} {{ 'contact.postal-code-invalid' | translate }}
</mat-error> </mat-error>
<mat-hint></mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field class="mat-block"> <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) { private updateView(value: Country | null) {
if (this.modelValue?.name !== value?.name) { if (this.modelValue?.name !== value?.name) {
this.modelValue = value; this.modelValue = value;
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue?.name);
if (value) { if (value) {
this.selectCountryCode.emit(value.iso2); 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 mat-dialog-content>
<div class="message-container"> <div class="message-container">
<span> <span>
{{ 'entity.version-conflict.link' | translate: {{ 'entity.version-conflict.link' | translate:{ entityType: entityTypeLabel | translate } }}
{ entityType: (entityTypeTranslations.get(entityId.entityType).type | translate) }
}}
<a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>. <a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>.
</span> </span>
<br/> <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 { export class EntityConflictDialogComponent {
entityId: EntityId; entityId: EntityId;
entityTypeLabel: string;
readonly entityTypeTranslations = entityTypeTranslations; readonly entityTypeTranslations = entityTypeTranslations;
private readonly defaultEntityLabel = 'entity.entity';
constructor( constructor(
@Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData, @Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData,
@ -51,6 +53,9 @@ export class EntityConflictDialogComponent {
private importExportService: ImportExportService, private importExportService: ImportExportService,
) { ) {
this.entityId = (data.entity as EntityInfoData).id ?? (data.entity as RuleChainMetaData).ruleChainId; 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 { onCancel(): void {

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

@ -17,7 +17,7 @@
import { import {
AfterViewInit, ChangeDetectorRef, AfterViewInit, ChangeDetectorRef,
Component, Component,
Input, Input, NgZone,
OnChanges, OnDestroy, OnChanges, OnDestroy,
OnInit, OnInit,
Renderer2, Renderer2,
@ -90,7 +90,8 @@ export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChang
constructor(private breakpointObserver: BreakpointObserver, constructor(private breakpointObserver: BreakpointObserver,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private renderer: Renderer2) { private renderer: Renderer2,
private zone: NgZone) {
} }
ngOnInit(): void { 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'); this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'padding', this.gap + 'px');
if (!(typeof this.itemSize === 'number')) { if (!(typeof this.itemSize === 'number')) {
this.contentResize$ = new ResizeObserver(() => { this.contentResize$ = new ResizeObserver(() => {
this.onContentResize(); this.zone.run(() => {
this.onContentResize();
});
}); });
this.contentResize$.observe(this.viewport._contentWrapper.nativeElement); 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, ElementRef,
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Input, Input, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
@ -196,7 +196,8 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
private importExportService: ImportExportService, private importExportService: ImportExportService,
private elementRef: ElementRef, private elementRef: ElementRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private fb: FormBuilder) { private fb: FormBuilder,
private zone: NgZone) {
super(store); super(store);
this.gridImagesFetchFunction = (pageSize, page, filter) => { this.gridImagesFetchFunction = (pageSize, page, filter) => {
@ -361,11 +362,13 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
private initListMode() { private initListMode() {
this.destroyListMode$ = new Subject<void>(); this.destroyListMode$ = new Subject<void>();
this.widgetResize$ = new ResizeObserver(() => { this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; this.zone.run(() => {
if (showHidePageSize !== this.hidePageSize) { const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
this.hidePageSize = showHidePageSize; if (showHidePageSize !== this.hidePageSize) {
this.cd.markForCheck(); this.hidePageSize = showHidePageSize;
} this.cd.markForCheck();
}
});
}); });
this.widgetResize$.observe(this.elementRef.nativeElement); this.widgetResize$.observe(this.elementRef.nativeElement);
if (this.pageMode) { if (this.pageMode) {

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

@ -25,7 +25,7 @@ import {
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Input, Input, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
@ -213,7 +213,8 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterV
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private platform: Platform, private platform: Platform,
private breakpointObserver: BreakpointObserver) { private breakpointObserver: BreakpointObserver,
private zone: NgZone) {
super(store); super(store);
} }
@ -296,7 +297,9 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterV
private startObservePagination() { private startObservePagination() {
this.toggleGroupResize$ = new ResizeObserver(() => { this.toggleGroupResize$ = new ResizeObserver(() => {
this.updatePagination(); this.zone.run(() => {
this.updatePagination();
});
}); });
this.toggleGroupResize$.observe(this.toggleGroupContainer.nativeElement); 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 { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { import { EntityInfoData, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
EntityInfoData,
ImportEntitiesResultInfo,
ImportEntityData
} from '@shared/models/entity.models';
import { RequestConfig } from '@core/http/http-utils'; import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service'; 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 { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions';
import { ExportableEntity } from '@shared/models/base-data'; import { ExportableEntity } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { Customer } from '@shared/models/customer.model';
export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean, export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>; customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
@ -391,6 +388,10 @@ export class ImportExportService {
case EntityType.DASHBOARD: case EntityType.DASHBOARD:
preparedData = this.prepareDashboardExport(entityData as Dashboard); preparedData = this.prepareDashboardExport(entityData as Dashboard);
break; break;
case EntityType.CUSTOMER:
preparedData = this.prepareExport({...entityData, name: (entityData as Customer).title});
(entityData as EntityInfoData).name = (entityData as Customer).title;
break;
default: default:
preparedData = this.prepareExport(entityData); 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 { UserId } from '@shared/models/id/user-id';
import { AlarmFilter } from '@shared/models/query/query.models'; import { AlarmFilter } from '@shared/models/query/query.models';
import { HasTenantId } from '@shared/models/entity.models'; import { HasTenantId } from '@shared/models/entity.models';
import { isNotEmptyStr } from '@core/utils';
export enum AlarmsMode { export enum AlarmsMode {
ALL, ALL,
@ -127,7 +128,7 @@ export interface AlarmComment extends BaseData<AlarmCommentId> {
text: string; text: string;
edited?: boolean; edited?: boolean;
editedOn?: number; editedOn?: number;
} };
} }
export interface AlarmCommentInfo extends AlarmComment { export interface AlarmCommentInfo extends AlarmComment {
@ -173,9 +174,9 @@ export const simulatedAlarm: AlarmInfo = {
originatorName: 'Simulated', originatorName: 'Simulated',
originatorLabel: 'Simulated', originatorLabel: 'Simulated',
assignee: { assignee: {
firstName: "", firstName: '',
lastName: "", lastName: '',
email: "test@example.com", email: 'test@example.com',
}, },
originator: { originator: {
entityType: EntityType.DEVICE, 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, "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>", "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, "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", "title": "Tenants",
"showTitleIcon": false, "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-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.",
"mobile-package-unique": "Application package must be unique.", "mobile-package-unique": "Application package must be unique.",
"mobile-package-max-length": "Application package should be less than 256", "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": "Application secret",
"mobile-app-secret-hint": "Base64 encoded string representing at least 512 bits of data.", "mobile-app-secret-hint": "Base64 encoded string representing at least 512 bits of data.",
"mobile-app-secret-required": "Application secret is required.", "mobile-app-secret-required": "Application secret is required.",
@ -594,6 +595,7 @@
"assignee-last-name": "Assignee last name", "assignee-last-name": "Assignee last name",
"assignee-email": "Assignee email", "assignee-email": "Assignee email",
"unassigned": "Unassigned", "unassigned": "Unassigned",
"user-deleted": "User deleted",
"assignee-not-set": "All", "assignee-not-set": "All",
"status": "Status", "status": "Status",
"alarm-details": "Alarm details", "alarm-details": "Alarm details",
@ -2934,6 +2936,7 @@
"details": "Details", "details": "Details",
"delete-mapping-title": "Delete mapping?", "delete-mapping-title": "Delete mapping?",
"delete-slave-title": "Delete slave?", "delete-slave-title": "Delete slave?",
"divider": "Divider",
"download-configuration-file": "Download configuration file", "download-configuration-file": "Download configuration file",
"download-docker-compose": "Download docker-compose.yml for your gateway", "download-docker-compose": "Download docker-compose.yml for your gateway",
"enable-remote-logging": "Enable remote logging", "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-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-min": "Min pack send delay can not be less then 10",
"min-pack-send-delay-pattern": "Min pack send delay is not valid", "min-pack-send-delay-pattern": "Min pack send delay is not valid",
"multiplier": "Multiplier",
"mode": "Mode", "mode": "Mode",
"model-name": "Model name", "model-name": "Model name",
"modifier": "Modifier",
"modifier-invalid": "Modifier is not valid",
"mqtt-version": "MQTT version", "mqtt-version": "MQTT version",
"name": "Name", "name": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",
@ -3254,6 +3260,7 @@
"sub-check-period": "Subscription check period (ms)", "sub-check-period": "Subscription check period (ms)",
"sub-check-period-error": "Subscription check period should be at least {{min}} (ms).", "sub-check-period-error": "Subscription check period should be at least {{min}} (ms).",
"security": "Security", "security": "Security",
"security-policy": "Security policy",
"security-type": "Security type", "security-type": "Security type",
"security-types": { "security-types": {
"access-token": "Access Token", "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.", "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.", "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.", "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.", "scan-period": "Period in milliseconds to rescan the server.",
"sub-check-period": "Period to check the subscriptions in the OPC-UA 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.", "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.", "objects-count": "Depends on the selected type.",
"address": "Register address to verify.", "address": "Register address to verify.",
"key": "Key to be used as the attribute key for the platform instance.", "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-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
min-height: 40px; min-height: 40px;
height: 40px;
width: auto; width: auto;
.mdc-text-field__input, .mat-mdc-select { .mdc-text-field__input, .mat-mdc-select {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
vertical-align: middle;
} }
} }
.mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix, .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 { &.tb-mat-28 {
@include tb-mat-icon-size(28); @include tb-mat-icon-size(28);
} }
&.tb-mat-30 {
@include tb-mat-icon-size(30);
}
&.tb-mat-32 { &.tb-mat-32 {
@include tb-mat-icon-size(32); @include tb-mat-icon-size(32);
} }

Loading…
Cancel
Save