Browse Source

UI: Fixed gateway dashboard and widget after review

pull/9242/head
Vladyslav_Prykhodko 3 years ago
parent
commit
0b9df293cd
  1. 3
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 3
      ui-ngx/src/app/core/http/device.service.ts
  3. 5
      ui-ngx/src/app/core/utils.ts
  4. 47
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html
  5. 34
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts
  6. 1444
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  7. 113
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  8. 267
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  9. 319
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  10. 66
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  11. 204
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  12. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html
  13. 155
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts
  14. 75
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html
  15. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts
  16. 74
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  17. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  18. 69
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  19. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html
  20. 119
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts
  21. 142
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  22. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html
  23. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts
  24. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html
  25. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts
  26. 14
      ui-ngx/src/app/shared/components/file-input.component.html
  27. 4
      ui-ngx/src/app/shared/components/file-input.component.scss
  28. 9
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  29. 5
      ui-ngx/src/form.scss

3
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -12,11 +12,10 @@
"gateway_widgets.attributes_card", "gateway_widgets.attributes_card",
"gateway_widgets.gateway_configuration2", "gateway_widgets.gateway_configuration2",
"gateway_widgets.config_form_latest", "gateway_widgets.config_form_latest",
"gateway_widgets.gateway_events",
"gateway_widgets.gateway_connector", "gateway_widgets.gateway_connector",
"gateway_widgets.gateway_logs", "gateway_widgets.gateway_logs",
"gateway_widgets.gateway_statistics", "gateway_widgets.gateway_statistics",
"gateway_widgets.gateway_general_statistics", "gateway_widgets.gateway_general_statistics",
"gateway_widgets.service_rpc" "gateway_widgets.service_rpc"
] ]
} }

3
ui-ngx/src/app/core/http/device.service.ts

@ -27,7 +27,8 @@ import {
DeviceCredentials, DeviceCredentials,
DeviceInfo, DeviceInfo,
DeviceInfoQuery, DeviceInfoQuery,
DeviceSearchQuery, PublishLaunchCommand, DeviceSearchQuery,
PublishLaunchCommand,
PublishTelemetryCommand PublishTelemetryCommand
} from '@app/shared/models/device.models'; } from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';

5
ui-ngx/src/app/core/utils.ts

@ -812,3 +812,8 @@ export const getOS = (): string => {
return os; return os;
}; };
export const camelCase = (str: string): string => {
return _.camelCase(str);
};

47
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent" style="padding-top: 5px"> <div mat-dialog-content fxLayout="column" style="padding: 0">
<div fxLayout="row" fxLayoutAlign="space-between center"> <div fxLayout="row" fxLayoutAlign="space-between center">
<span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span> <span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span>
<div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div> <div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div>
@ -29,14 +29,9 @@
Windows Windows
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.windows }">
```bash </ng-container>
{{windowsCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
@ -45,14 +40,9 @@
Linux Linux
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
```bash </ng-container>
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
@ -61,15 +51,22 @@
MacOS MacOS
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
```bash </ng-container>
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
<ng-template #commandsExample let-command="command">
<div class="tb-form-panel no-padding no-border tb-tab-body">
<tb-markdown usePlainMarkdown containerClass="start-code"
data="
```bash
{{ command }}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template>

34
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts

@ -15,15 +15,10 @@
/// ///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DeviceService } from '@core/http/device.service'; import { DeviceService } from '@core/http/device.service';
import { helpBaseUrl } from '@shared/models/constants'; import { helpBaseUrl } from '@shared/models/constants';
import { getOS } from '@core/utils'; import { getOS } from '@core/utils';
import { PublishLaunchCommand } from '@shared/models/device.models';
@Component({ @Component({
selector: 'tb-gateway-command', selector: 'tb-gateway-command',
@ -39,17 +34,13 @@ export class DeviceGatewayCommandComponent implements OnInit {
@Input() @Input()
deviceId: string; deviceId: string;
linuxCode: string; commands: PublishLaunchCommand;
windowsCode: string;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/'; helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
tabIndex = 0; tabIndex = 0;
constructor(protected router: Router, constructor(private cd: ChangeDetectorRef,
protected store: Store<AppState>,
private translate: TranslateService,
private cd: ChangeDetectorRef,
private deviceService: DeviceService) { private deviceService: DeviceService) {
} }
@ -57,7 +48,7 @@ export class DeviceGatewayCommandComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
if (this.deviceId) { if (this.deviceId) {
this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => { this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => {
this.createRunCode(commands.mqtt); this.commands = commands;
this.cd.detectChanges(); this.cd.detectChanges();
}); });
} }
@ -78,21 +69,4 @@ export class DeviceGatewayCommandComponent implements OnInit {
this.tabIndex = 1; this.tabIndex = 1;
} }
} }
createRunCode(commands) {
this.linuxCode = commands.linux;
this.windowsCode = commands.windows;
}
onDockerCodeCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('gateway.command-copied-message'),
type: 'success',
target: 'dockerCommandDialogContent',
duration: 1200,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
} }

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

File diff suppressed because it is too large

113
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss

@ -16,14 +16,13 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: flex;
flex-direction: column;
.mat-icon { .configuration-block {
color: rgba(0, 0, 0, .12); display: flex;
} flex-direction: column;
gap: 16px;
.tb-form-panel {
margin-bottom: 20px;
} }
.mat-toolbar { .mat-toolbar {
@ -31,113 +30,37 @@
color: rgba(0, 0, 0, .87) !important; color: rgba(0, 0, 0, .87) !important;
} }
.mat-content { .toggle-group {
.expansion-panel-header {
font-weight: 600;
color: rgba(0, 0, 0, .87) !important;
}
mat-slide-toggle {
margin-bottom: 16px;
}
mat-form-field {
margin-right: 15px;
}
.slider-icon {
position: absolute;
transform: translateY(-3px);
}
.block-title {
font-size: 20px;
font-weight: 400;
padding-top: 16px;
}
.hover-cursor {
cursor: pointer;
}
}
.security-toggle-group {
margin-bottom: 15px;
margin-right: auto; margin-right: auto;
} }
.logs-label {
font-weight: 500;
margin-bottom: 10px;
}
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.first-capital { .first-capital {
text-transform: capitalize; text-transform: capitalize;
} }
mat-panel-title {
display: block;
padding-top: 20px;
}
mat-panel-title span {
display: block;
padding-left: 0;
padding-top: 5px;
}
.tb-hint {
font-size: 13px;
color: rgba(0, 0, 0, .54);
width: fit-content;
cursor: pointer;
text-transform: none !important;
}
.line-break {
width: 100%;
}
textarea { textarea {
resize: none; resize: none;
} }
}
:host ::ng-deep {
.mat-tab-label-active {
color: white;
opacity: 1;
}
.mat-tab-label, .mat-tab-label-active{
min-width: 50px !important;
padding: 3px !important;
margin: 3px !important;
flex-grow: 1;
}
.mat-ink-bar { .saving-period {
height: 100%; flex: 1;
z-index: -10;
border-radius: 5px;
} }
}
:host ::ng-deep {
.pointer-event { .pointer-event {
pointer-events: all; pointer-events: all;
} }
.mat-mdc-form-field-icon-suffix { .toggle-group span {
z-index: 100; padding: 0 25px;
} }
.security-toggle-group span { .mat-mdc-form-field-icon-suffix {
padding: 0 25px; color: #E0E0E0;
&:hover {
color: #9E9E9E;
}
} }
} }

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

@ -15,15 +15,11 @@
/// ///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { import {
FormArray, FormArray,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
ValidatorFn, ValidatorFn,
Validators Validators
@ -41,79 +37,17 @@ import { Observable, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators'; import { mergeMap } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import {
export enum StorageTypes { GatewayLogLevel,
MEMORY = 'memory', GecurityTypesTranslationsMap,
FILE = 'file', LocalLogsConfigTranslateMap,
SQLITE = 'sqlite' LocalLogsConfigs,
} LogSavingPeriod,
LogSavingPeriodTranslations,
export enum GatewayLogLevel { SecurityTypes,
none = 'NONE', StorageTypes,
critical = 'CRITICAL', StorageTypesTranslationMap
error = 'ERROR', } from './gateway-widget.models';
warning = 'WARNING',
info = 'INFO',
debug = 'DEBUG'
}
export enum LogSavingPeriod {
days = 'D',
hours = 'H',
minutes = 'M',
seconds = 'S'
}
export enum LocalLogsConfigs {
service = 'service',
connector = 'connector',
converter = 'converter',
tb_connection = 'tb_connection',
storage = 'storage',
extension = 'extension'
}
export const localLogsConfigLabels = new Map<LocalLogsConfigs, string>([
[LocalLogsConfigs.service, 'Service'],
[LocalLogsConfigs.connector, 'Connector'],
[LocalLogsConfigs.converter, 'Converter'],
[LocalLogsConfigs.tb_connection, 'TB Connection'],
[LocalLogsConfigs.storage, 'Storage'],
[LocalLogsConfigs.extension, 'Extension']
]);
export const logSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[
[LogSavingPeriod.days, 'gateway.logs.days'],
[LogSavingPeriod.hours, 'gateway.logs.hours'],
[LogSavingPeriod.minutes, 'gateway.logs.minutes'],
[LogSavingPeriod.seconds, 'gateway.logs.seconds']
]
);
export const storageTypesTranslations = new Map<StorageTypes, string>(
[
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
[StorageTypes.FILE, 'gateway.storage-types.file-storage'],
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
]
);
export enum SecurityTypes {
ACCESS_TOKEN = 'accessToken',
USERNAME_PASSWORD = 'usernamePassword',
TLS_ACCESS_TOKEN = 'tlsAccessToken',
TLS_PRIVATE_KEY = 'tlsPrivateKey'
}
export const securityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
]
);
@Component({ @Component({
selector: 'tb-gateway-configuration', selector: 'tb-gateway-configuration',
@ -124,13 +58,16 @@ export class GatewayConfigurationComponent implements OnInit {
gatewayConfigGroup: FormGroup; gatewayConfigGroup: FormGroup;
storageTypes = storageTypesTranslations; StorageTypes = StorageTypes;
storageTypes = Object.values(StorageTypes) as StorageTypes[];
storageTypesTranslationMap = StorageTypesTranslationMap;
logSavingPeriods = logSavingPeriodTranslations; logSavingPeriods = LogSavingPeriodTranslations;
localLogsConfigLabels = localLogsConfigLabels; localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[];
localLogsConfigTranslateMap = LocalLogsConfigTranslateMap;
securityTypes = securityTypesTranslationsMap; securityTypes = GecurityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel); gatewayLogLevel = Object.values(GatewayLogLevel);
@ -142,18 +79,13 @@ export class GatewayConfigurationComponent implements OnInit {
logSelector: FormControl; logSelector: FormControl;
securityType: SecurityTypes; private initialCredentials: DeviceCredentials;
initialCredentials: DeviceCredentials; constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
public dialog: MatDialog) { private dialog: MatDialog) {
} }
ngOnInit() { ngOnInit() {
@ -221,7 +153,7 @@ export class GatewayConfigurationComponent implements OnInit {
type: ['remote', [Validators.required]], type: ['remote', [Validators.required]],
remote: this.fb.group({ remote: this.fb.group({
enabled: [false], enabled: [false],
logLevel: [GatewayLogLevel.info, [Validators.required]], logLevel: [GatewayLogLevel.INFO, [Validators.required]],
}), }),
local: this.fb.group({}) local: this.fb.group({})
}) })
@ -231,7 +163,7 @@ export class GatewayConfigurationComponent implements OnInit {
if (password && password !== '') { if (password && password !== '') {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]); this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]);
} else { } else {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([]); this.gatewayConfigGroup.get('thingsboard.security.username').clearValidators();
} }
this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false});
}); });
@ -258,10 +190,10 @@ export class GatewayConfigurationComponent implements OnInit {
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]);
} else { } else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setErrors(null);
} }
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({emitEvent: false});
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false});
}); });
this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => { this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => {
@ -287,16 +219,14 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('caCert').addValidators([Validators.required]); securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity(); securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) { } else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]) securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
// securityGroup.get('password').addValidators([Validators.required]);
// securityGroup.get('password').updateValueAndValidity();
} }
securityGroup.updateValueAndValidity(); securityGroup.updateValueAndValidity();
}); });
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('caCert').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges());
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => { storageGroup.get('type').valueChanges.subscribe(type => {
@ -306,6 +236,8 @@ export class GatewayConfigurationComponent implements OnInit {
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]); [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators( storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.FILE) { } else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required]); storageGroup.get('data_folder_path').addValidators([Validators.required]);
storageGroup.get('max_file_count').addValidators( storageGroup.get('max_file_count').addValidators(
@ -314,20 +246,27 @@ export class GatewayConfigurationComponent implements OnInit {
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators( storageGroup.get('max_records_per_file').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_folder_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_file_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.SQLITE) { } else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required]); storageGroup.get('data_file_path').addValidators([Validators.required]);
storageGroup.get('messages_ttl_check_in_hours').addValidators( storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators( storageGroup.get('messages_ttl_in_days').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_file_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_check_in_hours').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false});
} }
}); });
this.fetchConfigAttribute(this.device); this.fetchConfigAttribute(this.device);
} }
atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: UntypedFormGroup): ValidationErrors | null => { return (group: FormGroup): ValidationErrors | null => {
if (!controls) { if (!controls) {
controls = Object.keys(group.controls); controls = Object.keys(group.controls);
} }
@ -337,22 +276,10 @@ export class GatewayConfigurationComponent implements OnInit {
}; };
} }
updateSecurityValidators(value: SecurityTypes) { private fetchConfigAttribute(entityId: EntityId) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(value, {emitEvent: true}); if (entityId.id === NULL_UUID) {
this.gatewayConfigGroup.get('thingsboard.security.type').markAsDirty(); return;
} }
updateLogType(value: LocalLogsConfigs) {
this.logSelector.setValue(value);
}
updateStorageType(value: StorageTypes) {
this.gatewayConfigGroup.get('storage.type').setValue(value, {emitEvent: true});
this.gatewayConfigGroup.get('storage.type').markAsDirty();
}
fetchConfigAttribute(entityId: EntityId) {
if (entityId.id === NULL_UUID) return;
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE,
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe(
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes(
@ -397,24 +324,26 @@ export class GatewayConfigurationComponent implements OnInit {
if (remoteLoggingLevel) { if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({ remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none, enabled: remoteLoggingLevel !== GatewayLogLevel.NONE,
logLevel: remoteLoggingLevel logLevel: remoteLoggingLevel
}, {emitEvent: false}); }, {emitEvent: false});
remoteLogsFormGroup.markAsPristine(); remoteLogsFormGroup.markAsPristine();
} }
this.cd.detectChanges(); this.cd.detectChanges();
} else { } else {
this.checkAndFetchCredentials({}); this.checkAndFetchCredentials();
} }
}); });
} }
checkAndFetchCredentials(security): void { private checkAndFetchCredentials(security: any = {}): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
this.initialCredentials = credentials; this.initialCredentials = credentials;
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) { if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN? SecurityTypes.TLS_ACCESS_TOKEN : SecurityTypes.ACCESS_TOKEN); this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN
? SecurityTypes.TLS_ACCESS_TOKEN
: SecurityTypes.ACCESS_TOKEN);
this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId); this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId);
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) { if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert); this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert);
@ -432,7 +361,7 @@ export class GatewayConfigurationComponent implements OnInit {
} }
} }
logsToObj(logsConfig) { private logsToObj(logsConfig: any) {
const logsObject = { const logsObject = {
local: {} local: {}
}; };
@ -453,43 +382,42 @@ export class GatewayConfigurationComponent implements OnInit {
return {local: logsObject, logFormat, dateFormat}; return {local: logsObject, logFormat, dateFormat};
} }
toggleRpcFields(enable: boolean) { private toggleRpcFields(enable: boolean) {
const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup; const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup;
if (enable) { if (enable) {
grpcGroup.get('serverPort').enable(); grpcGroup.get('serverPort').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').enable(); grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').enable(); grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').enable(); grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').enable(); grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').enable(); grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').enable(); grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false});
} else { } else {
grpcGroup.get('serverPort').disable(); grpcGroup.get('serverPort').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').disable(); grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').disable(); grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').disable(); grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').disable(); grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').disable(); grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').disable(); grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false});
} }
} }
addCommand(command?): void { addCommand(command: any = {}): void {
const data = command || {};
const commandsFormArray = this.commandFormArray(); const commandsFormArray = this.commandFormArray();
const commandFormGroup = this.fb.group({ const commandFormGroup = this.fb.group({
attributeOnGateway: [data.attributeOnGateway || null, [Validators.required]], attributeOnGateway: [command.attributeOnGateway || null, [Validators.required]],
command: [data.command || null, [Validators.required]], command: [command.command || null, [Validators.required]],
timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
}); });
commandsFormArray.push(commandFormGroup); commandsFormArray.push(commandFormGroup);
} }
addLocalLogConfig(name, config): void { private addLocalLogConfig(name: string, config: any): void {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({ const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]], logLevel: [config.logLevel || GatewayLogLevel.INFO, [Validators.required]],
filePath: [config.filePath || './logs', [Validators.required]], filePath: [config.filePath || './logs', [Validators.required]],
backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]],
@ -507,12 +435,14 @@ export class GatewayConfigurationComponent implements OnInit {
} }
removeCommandControl(index: number, event: any): void { removeCommandControl(index: number, event: any): void {
if (event.pointerType === '') return; if (event.pointerType === '') {
return;
}
this.commandFormArray().removeAt(index); this.commandFormArray().removeAt(index);
this.gatewayConfigGroup.markAsDirty(); this.gatewayConfigGroup.markAsDirty();
} }
removeAllSecurityValidators(): void { private removeAllSecurityValidators(): void {
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.clearValidators(); securityGroup.clearValidators();
for (const controlsKey in securityGroup.controls) { for (const controlsKey in securityGroup.controls) {
@ -524,7 +454,7 @@ export class GatewayConfigurationComponent implements OnInit {
} }
} }
removeAllStorageValidators(): void { private removeAllStorageValidators(): void {
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
for (const storageKey in storageGroup.controls) { for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') { if (storageKey !== 'type') {
@ -535,7 +465,7 @@ export class GatewayConfigurationComponent implements OnInit {
} }
} }
removeEmpty(obj) { private removeEmpty(obj: any) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(obj) Object.entries(obj)
.filter(([_, v]) => v != null) .filter(([_, v]) => v != null)
@ -543,7 +473,7 @@ export class GatewayConfigurationComponent implements OnInit {
); );
} }
generateLogsFile(logsObj) { private generateLogsFile(logsObj: any) {
const logAttrObj = { const logAttrObj = {
version: 1, version: 1,
disable_existing_loggers: false, disable_existing_loggers: false,
@ -591,7 +521,7 @@ export class GatewayConfigurationComponent implements OnInit {
return logAttrObj; return logAttrObj;
} }
createHandlerObj(logObj, key) { private createHandlerObj(logObj: any, key: string) {
return { return {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter', formatter: 'LogFormatter',
@ -603,7 +533,7 @@ export class GatewayConfigurationComponent implements OnInit {
}; };
} }
createLoggerObj(logObj, key) { private createLoggerObj(logObj: any, key: string) {
return { return {
handlers: [`${key}Handler`, 'consoleHandler'], handlers: [`${key}Handler`, 'consoleHandler'],
level: logObj.logLevel, level: logObj.logLevel,
@ -617,7 +547,7 @@ export class GatewayConfigurationComponent implements OnInit {
const attributes = []; const attributes = [];
attributes.push({ attributes.push({
key: 'RemoteLoggingLevel', key: 'RemoteLoggingLevel',
value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.none value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE
}); });
delete value.connectors; delete value.connectors;
attributes.push({ attributes.push({
@ -642,18 +572,18 @@ export class GatewayConfigurationComponent implements OnInit {
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
this.updateCredentials(value.thingsboard.security).subscribe(_ => { this.updateCredentials(value.thingsboard.security).subscribe(() => {
if (this.dialogRef) { if (this.dialogRef) {
this.dialogRef.close(); this.dialogRef.close();
} else { } else {
this.gatewayConfigGroup.markAsPristine(); this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges(); this.cd.detectChanges();
} }
}) });
}); });
} }
updateCredentials(securityConfig): Observable<any> { private updateCredentials(securityConfig: any): Observable<any> {
let updateCredentials = false; let updateCredentials = false;
let newCredentials = {}; let newCredentials = {};
if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) { if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) {
@ -661,14 +591,23 @@ export class GatewayConfigurationComponent implements OnInit {
updateCredentials = true; updateCredentials = true;
} else { } else {
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue);
updateCredentials = !(parsedCredentials.clientId === securityConfig.clientId && parsedCredentials.userName === securityConfig.username && parsedCredentials.password === securityConfig.password); updateCredentials = !(
parsedCredentials.clientId === securityConfig.clientId &&
parsedCredentials.userName === securityConfig.username &&
parsedCredentials.password === securityConfig.password);
} }
if (updateCredentials) { if (updateCredentials) {
let credentialsValue: { clientId?, userName?, password? } = {}; const credentialsValue: { clientId?: string; userName?: string; password?: string } = {};
const credentialsType = DeviceCredentialsType.MQTT_BASIC; const credentialsType = DeviceCredentialsType.MQTT_BASIC;
if (securityConfig.clientId) credentialsValue.clientId = securityConfig.clientId; if (securityConfig.clientId) {
if (securityConfig.username) credentialsValue.userName = securityConfig.username; credentialsValue.clientId = securityConfig.clientId;
if (securityConfig.password) credentialsValue.password = securityConfig.password; }
if (securityConfig.username) {
credentialsValue.userName = securityConfig.username;
}
if (securityConfig.password) {
credentialsValue.password = securityConfig.password;
}
newCredentials = { newCredentials = {
credentialsType, credentialsType,
credentialsValue: JSON.stringify(credentialsValue) credentialsValue: JSON.stringify(credentialsValue)
@ -684,12 +623,12 @@ export class GatewayConfigurationComponent implements OnInit {
newCredentials = { newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN, credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken credentialsId: securityConfig.accessToken
} };
} }
} }
if (updateCredentials) { if (updateCredentials) {
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials}) return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials});
} }
return of(null); return of(null);
} }

319
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -15,82 +15,102 @@
limitations under the License. limitations under the License.
--> -->
<form style="height: calc(100% - 7px)" fxLayout="column"> <div class="connector-container tb-form-panel no-border">
<div fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxFlex class="connector-container"> <section class="table-container tb-form-panel stroked no-padding flex">
<mat-card fxLayout="column" fxFlex.lt-lg style="overflow: auto; min-height: 35vh"> <mat-toolbar class="mat-mdc-table-toolbar">
<mat-toolbar class="mat-mdc-table-toolbar"> <h2>{{ 'gateway.connectors' | translate }}</h2>
<h2>{{ 'gateway.connectors' | translate }}</h2> <span fxFlex></span>
<span fxFlex></span> <button mat-icon-button
<button mat-icon-button [disabled]="isLoading$ | async"
[disabled]="isLoading$ | async" (click)="addAttribute()"
(click)="addAttribute()" matTooltip="{{ 'action.add' | translate }}"
matTooltip="{{ 'action.add' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <mat-icon>add</mat-icon>
<mat-icon>add</mat-icon> </button>
</button> </mat-toolbar>
</mat-toolbar> <table mat-table [dataSource]="dataSource"
<table mat-table [dataSource]="dataSource" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
matSortDisableClear> <ng-container matColumnDef="enabled" sticky>
<ng-container matColumnDef="enabled" sticky> <mat-header-cell *matHeaderCellDef style="width: 60px;min-width: 60px;">
<mat-header-cell *matHeaderCellDef style="width: 30px;"> {{ 'gateway.connectors-table-enabled' | translate }}
{{ 'gateway.connectors-table-enabled' | translate }} </mat-header-cell>
</mat-header-cell> <mat-cell *matCellDef="let attribute">
<mat-cell *matCellDef="let attribute"> <mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)"
<mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)" (click)="$event.stopPropagation(); enableConnector(attribute)"></mat-slide-toggle>
(click)="$event.stopPropagation(); enableConnector(attribute)"></mat-slide-toggle> </mat-cell>
</mat-cell> </ng-container>
</ng-container> <ng-container matColumnDef="key">
<ng-container matColumnDef="key"> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> {{ 'gateway.connectors-table-name' | translate }}</mat-header-cell>
{{ 'gateway.connectors-table-name' | translate }}</mat-header-cell> <mat-cell *matCellDef="let attribute">
<mat-cell *matCellDef="let attribute"> {{ attribute.key }}
{{ attribute.key }} </mat-cell>
</mat-cell> </ng-container>
</ng-container> <ng-container matColumnDef="type">
<ng-container matColumnDef="type"> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> {{ 'gateway.connectors-table-type' | translate }}
{{ 'gateway.connectors-table-type' | translate }} </mat-header-cell>
</mat-header-cell> <mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> {{ returnType(attribute) }}
{{ returnType(attribute) }} </mat-cell>
</mat-cell> </ng-container>
</ng-container> <ng-container matColumnDef="syncStatus">
<ng-container matColumnDef="syncStatus"> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> {{ 'gateway.configuration' | translate }}
{{ 'gateway.configuration' | translate }} </mat-header-cell>
</mat-header-cell> <mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> <div class="status" [class]="isConnectorSynced(attribute) ? 'status-sync' : 'status-unsync'">
<div {{ isConnectorSynced(attribute) ? 'sync' : 'out of sync' }}
[ngClass]="{ </div>
'status-block': true, </mat-cell>
'status-sync':isConnectorSynced(attribute), </ng-container>
'status-unsync':!isConnectorSynced(attribute) <ng-container matColumnDef="errors">
}">{{isConnectorSynced(attribute)?'sync' : 'out of sync'}}</div> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
</mat-cell> {{ 'gateway.connectors-table-status' | translate }}
</ng-container> </mat-header-cell>
<ng-container matColumnDef="errors"> <mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> <span class="dot"
{{ 'gateway.connectors-table-status' | translate }} matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
</mat-header-cell> matTooltipPosition="above"
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> [class]="{'hasErrors': +getErrorsCount(attribute) > 0,
<span class="dot" 'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}" </mat-cell>
matTooltipPosition="above" </ng-container>
[ngClass]="{ <ng-container matColumnDef="actions" stickyEnd>
'hasErrors': +getErrorsCount(attribute) > 0, <mat-header-cell *matHeaderCellDef
'noErrors': +getErrorsCount(attribute) == 0 || getErrorsCount(attribute) == '' [ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
}"></span> {{ 'gateway.connectors-table-actions' | translate }}
</mat-cell> </mat-header-cell>
</ng-container> <mat-cell *matCellDef="let attribute"
<ng-container matColumnDef="actions" stickyEnd> [ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
<mat-header-cell *matHeaderCellDef <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}"> <button mat-icon-button
{{ 'gateway.connectors-table-actions' | translate }} matTooltip="RPC"
</mat-header-cell> matTooltipPosition="above"
<mat-cell *matCellDef="let attribute" (click)="connectorRpc(attribute, $event)">
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}"> <mat-icon>private_connectivity</mat-icon>
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> </button>
<button mat-icon-button
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button <button mat-icon-button
matTooltip="RPC" matTooltip="RPC"
matTooltipPosition="above" matTooltipPosition="above"
@ -98,7 +118,7 @@
<mat-icon>private_connectivity</mat-icon> <mat-icon>private_connectivity</mat-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
matTooltip="Logs" matTooltip="Delete connector"
matTooltipPosition="above" matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)"> (click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon> <mat-icon>list</mat-icon>
@ -109,90 +129,61 @@
(click)="deleteConnector(attribute, $event)"> (click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</div> </mat-menu>
<div fxHide fxShow.lt-lg> </div>
<button mat-icon-button </mat-cell>
(click)="$event.stopPropagation()" </ng-container>
[matMenuTriggerFor]="cellActionsMenu"> <mat-header-row class="mat-row-select"
<mat-icon class="material-icons">more_vert</mat-icon> *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
</button> <mat-row class="mat-row-select" [class]="{'tb-current-entity': isSameConnector(attribute)}"
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> *matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
<button mat-icon-button </table>
matTooltip="RPC" </section>
matTooltipPosition="above" <section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding flex">
(click)="connectorRpc(attribute, $event)"> <section class="tb-form-row tb-not-inline-field same-padding input-container">
<mat-icon>private_connectivity</mat-icon> <mat-form-field class="flex" subscriptSizing="dynamic">
</button> <mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label>
<button mat-icon-button <input matInput formControlName="name" #nameInput/>
matTooltip="Delete connector" </mat-form-field>
matTooltipPosition="above" <mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
(click)="connectorLogs(attribute, $event)"> <mat-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label>
<mat-icon>list</mat-icon> <mat-select formControlName="type">
</button> <mat-option style="text-transform: uppercase"
<button mat-icon-button *ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{ type.value }}</mat-option>
matTooltip="Delete connector" </mat-select>
matTooltipPosition="above" </mat-form-field>
(click)="deleteConnector(attribute, $event)"> <mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'grpc'" subscriptSizing="dynamic">
<mat-icon>delete</mat-icon> <mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label>
</button> <input matInput formControlName="key"/>
</mat-menu> </mat-form-field>
</div> <mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'custom'" subscriptSizing="dynamic">
</mat-cell> <mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label>
</ng-container> <input matInput formControlName="class"/>
<mat-header-row [ngClass]="{'mat-row-select': true}" </mat-form-field>
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<mat-row [ngClass]="{'mat-row-select': true, <mat-label translate>gateway.remote-logging-level</mat-label>
'tb-current-entity': isSameConnector(attribute)}" <mat-select formControlName="logLevel">
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row> <mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</table> </mat-select>
<mat-divider></mat-divider> </mat-form-field>
</mat-card> </section>
<div [formGroup]="connectorForm" fxLayout="column"> <section class="tb-form-panel stroked flex">
<mat-card fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxLayoutGap.lt-lg="5px"> <tb-json-object-edit
<mat-form-field class="mat-block tb-value-type"> class="flex"
<mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label> fxLayout="column"
<input matInput formControlName="name" #nameInput/> required
</mat-form-field> label="{{ 'gateway.configuration' | translate }}"
<mat-form-field class="mat-block tb-value-type"> formControlName="configurationJson">
<mat-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label> </tb-json-object-edit>
<mat-select formControlName="type"> <div fxLayoutAlign="start center">
<mat-option style="text-transform: uppercase" <button mat-raised-button color="primary"
*ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{type.value}}</mat-option> class="action-button"
</mat-select> type="button"
</mat-form-field> [disabled]="!connectorForm.dirty || connectorForm.invalid"
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'grpc'"> (click)="saveConnector()">
<mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label> {{ 'action.save' | translate }}
<input matInput formControlName="key"/> </button>
</mat-form-field> </div>
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'custom'"> </section>
<mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label> </section>
<input matInput formControlName="class"/> </div>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type">
<mat-label translate>gateway.remote-logging-level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option>
</mat-select>
</mat-form-field>
</mat-card>
<mat-card fxLayout="column" fxFlex>
<tb-json-object-edit
fxFlex
fxLayout="column"
required
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<div fxLayoutAlign="start center">
<button mat-raised-button color="primary"
class="action-btns"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</mat-card>
</div>
</div>
</form>

66
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../../../scss/constants';
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -21,25 +23,24 @@
padding: 0; padding: 0;
.connector-container { .connector-container {
height: 100%;
width: 100%; width: 100%;
flex-direction: row;
@media #{$mat-lt-lg} {
flex-direction: column;
}
& > mat-card, & > div { .table-container {
min-width: calc(50% - 15px); overflow: auto;
min-height: 35vh;
} }
mat-card { .flex {
margin: 10px; flex: 1;
padding: 10px;
max-width: 100%;
} }
}
.tb-entity-table { .input-container {
.tb-entity-table-content { height: auto;
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
} }
} }
@ -48,43 +49,28 @@
color: rgba(0, 0, 0, .87) !important; color: rgba(0, 0, 0, .87) !important;
} }
.mat-mdc-form-field {
flex-grow: 1;
}
mat-card {
padding-left: 10px;
background: transparent;
}
.mat-card-selected {
background-color: rgba(48, 86, 128, 0.1);
}
.mat-mdc-slide-toggle { .mat-mdc-slide-toggle {
margin: 15px; margin: 0 8px;
} }
.status-block { .status {
text-align: center; text-align: center;
border-radius: 16px; border-radius: 16px;
font-weight: 500; font-weight: 500;
width: fit-content; width: fit-content;
padding: 5px 15px; padding: 5px 15px;
&-sync {
background: rgba(25, 128, 56, .06);
color: rgb(25, 128, 56);
}
&-unsync {
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
} }
.status-sync { .action-button {
background: rgba(25, 128, 56, .06); margin: 0 15px;
color: rgb(25, 128, 56);
}
.status-unsync {
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
.action-btns {
margin: 10px 10px 0;
} }
mat-row { mat-row {

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

@ -17,14 +17,11 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
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 { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { forkJoin } from 'rxjs'; import { forkJoin, Observable } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
@ -33,47 +30,20 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { deepClone } from '@core/utils'; import { camelCase, deepClone, isString } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models'; import { DatasourceType, widgetType } from '@shared/models/widget.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 {
GatewayConnector,
export interface gatewayConnector { GatewayConnectorDefaultTypesTranslates,
name: string; GatewayLogLevel
type: string; } from './gateway-widget.models';
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([
['mqtt', 'MQTT'],
['modbus', 'MODBUS'],
['grpc', 'GRPC'],
['opcua', 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'],
['ble', 'BLE'],
['request', 'REQUEST'],
['can', 'CAN'],
['bacnet', 'BACNET'],
['odbc', 'ODBC'],
['rest', 'REST'],
['snmp', 'SNMP'],
['ftp', 'FTP'],
['socket', 'SOCKET'],
['xmpp', 'XMPP'],
['ocpp', 'OCCP'],
['custom', 'CUSTOM']
]);
@Component({ @Component({
selector: 'tb-gateway-connector', selector: 'tb-gateway-connector',
@ -84,12 +54,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
pageLink: PageLink; pageLink: PageLink;
attributeDataSource: AttributeDatasource;
inactiveConnectorsDataSource: AttributeDatasource;
serverDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>; dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
@ -107,27 +71,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connectorForm: FormGroup; connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean; textSearchMode: boolean;
activeConnectors: Array<string>; activeConnectors: Array<string>;
inactiveConnectors: Array<string>; gatewayLogLevel = Object.values(GatewayLogLevel);
InitialActiveConnectors: Array<string>; private inactiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel); private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource;
activeData: Array<any> = []; private serverDataSource: AttributeDatasource;
inactiveData: Array<any> = []; private activeData: Array<any> = [];
sharedAttributeData: Array<AttributeData> = []; private inactiveData: Array<any> = [];
initialConnector: gatewayConnector; private sharedAttributeData: Array<AttributeData> = [];
subscriptionOptions: WidgetSubscriptionOptions = { private initialConnector: GatewayConnector;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: { callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => { onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated(); this.onDataUpdated();
@ -138,20 +104,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
}; };
subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
constructor(protected router: Router, constructor(protected store: Store<AppState>,
protected store: Store<AppState>, private fb: FormBuilder,
protected fb: FormBuilder, private translate: TranslateService,
protected translate: TranslateService, private attributeService: AttributeService,
protected attributeService: AttributeService, private dialogService: DialogService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService, private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone, private zone: NgZone,
private utils: UtilsService, private utils: UtilsService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef) {
public dialog: MatDialog) {
super(store); super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC}; const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 0, null, sortOrder); this.pageLink = new PageLink(1000, 0, null, sortOrder);
@ -186,16 +149,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return data[sortHeaderId] || data.value[sortHeaderId]; return data[sortHeaderId] || data.value[sortHeaderId];
}; };
this.viewsInited = true;
if (this.device) { if (this.device) {
if (this.device.id === NULL_UUID) return; if (this.device.id === NULL_UUID) {
forkJoin(this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']), return;
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])).subscribe(attributes => { }
forkJoin([
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])
]).subscribe(attributes => {
if (attributes.length) { if (attributes.length) {
this.activeConnectors = attributes[0].length ? attributes[0][0].value : []; this.activeConnectors = attributes[0].length ? attributes[0][0].value : [];
this.activeConnectors = typeof this.activeConnectors === 'string' ? JSON.parse(this.activeConnectors) : this.activeConnectors; this.activeConnectors = isString(this.activeConnectors) ? JSON.parse(this.activeConnectors as any) : this.activeConnectors;
this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : []; this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : [];
this.inactiveConnectors = typeof this.inactiveConnectors === 'string' ? JSON.parse(this.inactiveConnectors) : this.inactiveConnectors; this.inactiveConnectors = isString(this.inactiveConnectors)
? JSON.parse(this.inactiveConnectors as any)
: this.inactiveConnectors;
this.updateData(true); this.updateData(true);
} else { } else {
this.activeConnectors = []; this.activeConnectors = [];
@ -206,7 +174,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
} }
uniqNameRequired(): ValidatorFn { private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => { return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase(); const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => { const found = this.dataSource.data.find((connectorAttr) => {
@ -229,7 +197,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
saveConnector(): void { saveConnector(): void {
const value = this.connectorForm.value; const value = this.connectorForm.value;
value.configuration = this.camelize(value.name) + '.json'; value.configuration = camelCase(value.name) + '.json';
if (value.type !== 'grpc') { if (value.type !== 'grpc') {
delete value.key; delete value.key;
} }
@ -242,7 +210,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value value
}]; }];
const attributesToDelete = []; const attributesToDelete = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
let updateActiveConnectors = false; let updateActiveConnectors = false;
if (this.initialConnector && this.initialConnector.name !== value.name) { if (this.initialConnector && this.initialConnector.name !== value.name) {
attributesToDelete.push({key: this.initialConnector.name}); attributesToDelete.push({key: this.initialConnector.name});
@ -256,19 +226,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.inactiveConnectors.splice(inactiveIndex, 1); this.inactiveConnectors.splice(inactiveIndex, 1);
} }
} }
if (!this.activeConnectors.includes(value.name) && scope == AttributeScope.SHARED_SCOPE) { if (!this.activeConnectors.includes(value.name) && scope === AttributeScope.SHARED_SCOPE) {
this.activeConnectors.push(value.name); this.activeConnectors.push(value.name);
updateActiveConnectors = true; updateActiveConnectors = true;
} }
if (!this.inactiveConnectors.includes(value.name) && scope == AttributeScope.SERVER_SCOPE) { if (!this.inactiveConnectors.includes(value.name) && scope === AttributeScope.SERVER_SCOPE) {
this.inactiveConnectors.push(value.name); this.inactiveConnectors.push(value.name);
updateActiveConnectors = true; updateActiveConnectors = true;
} }
const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)]; const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)];
if (updateActiveConnectors) { if (updateActiveConnectors) {
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}])); }]));
} }
@ -282,7 +252,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
updateData(reload: boolean = false) { private updateData(reload: boolean = false) {
this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => { this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => {
@ -301,26 +271,30 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
isConnectorSynced(attribute: AttributeData) { isConnectorSynced(attribute: AttributeData) {
const connectorData = attribute.value; const connectorData = attribute.value;
if (!connectorData.ts) return false; if (!connectorData.ts) {
return false;
}
const clientIndex = this.activeData.findIndex(data => { const clientIndex = this.activeData.findIndex(data => {
const sharedData = data.value; const sharedData = data.value;
return sharedData.name === connectorData.name; return sharedData.name === connectorData.name;
}) });
if (clientIndex == -1) return false; if (clientIndex === -1) {
return false;
}
const sharedIndex = this.sharedAttributeData.findIndex(data => { const sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value; const sharedData = data.value;
return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts; return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts;
}) });
return sharedIndex !== -1; return sharedIndex !== -1;
} }
combineData() { private combineData() {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key) index === self.findIndex((t) => t.key === item.key)
).map(attribute=>{ ).map(attribute => {
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value; attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value;
return attribute return attribute;
}); });
} }
@ -330,14 +304,13 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
this.nameInput.nativeElement.focus(); this.nameInput.nativeElement.focus();
this.clearOutConnectorForm(); this.clearOutConnectorForm();
} }
clearOutConnectorForm(): void { private clearOutConnectorForm(): void {
this.connectorForm.setValue({ this.connectorForm.setValue({
name: '', name: '',
type: 'mqtt', type: 'mqtt',
logLevel: GatewayLogLevel.info, logLevel: GatewayLogLevel.INFO,
key: 'auto', key: 'auto',
class: '', class: '',
configuration: '', configuration: '',
@ -347,7 +320,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
selectConnector(attribute): void { selectConnector(attribute: AttributeData): void {
if (this.connectorForm.disabled) { if (this.connectorForm.disabled) {
this.connectorForm.enable(); this.connectorForm.enable();
} }
@ -363,8 +336,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
isSameConnector(attribute): boolean { isSameConnector(attribute: AttributeData): boolean {
if (!this.initialConnector) return false; if (!this.initialConnector) {
return false;
}
const connector = attribute.value; const connector = attribute.value;
return this.initialConnector.name === connector.name; return this.initialConnector.name === connector.name;
} }
@ -378,12 +353,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
verticalPosition: 'top', verticalPosition: 'top',
horizontalPosition: 'right', horizontalPosition: 'right',
target: 'dashboardRoot', target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true forceDismiss: true
})); }));
} }
returnType(attribute) { returnType(attribute: AttributeData): string {
const value = attribute.value; const value = attribute.value;
return this.gatewayConnectorDefaultTypes.get(value.type); return this.gatewayConnectorDefaultTypes.get(value.type);
} }
@ -396,8 +370,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
const content = `All connector data will be deleted.`; const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) { if (result) {
const tasks = []; const tasks: Array<Observable<any>> = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute])); tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute]));
const activeIndex = this.activeConnectors.indexOf(attribute.key); const activeIndex = this.activeConnectors.indexOf(attribute.key);
const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key); const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key);
@ -408,10 +384,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.inactiveConnectors.splice(inactiveIndex, 1); this.inactiveConnectors.splice(inactiveIndex, 1);
} }
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}])); }]));
forkJoin(tasks).subscribe(_ => { forkJoin(tasks).subscribe(() => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) { if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm(); this.clearOutConnectorForm();
this.cd.detectChanges(); this.cd.detectChanges();
@ -423,10 +399,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
camelize(str): string {
return str.toLowerCase().replace(/\s+/g, '_');
}
connectorLogs(attribute: AttributeData, $event: Event): void { connectorLogs(attribute: AttributeData, $event: Event): void {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -448,7 +420,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
enableConnector(attribute): void { enableConnector(attribute: AttributeData): void {
const wasEnabled = this.activeConnectors.includes(attribute.key); const wasEnabled = this.activeConnectors.includes(attribute.key);
const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
const scopeNew = !wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scopeNew = !wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
@ -475,7 +447,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
onDataUpdateError(e: any) { private onDataUpdateError(e: any) {
const exceptionData = this.utils.parseException(e); const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name; let errorText = exceptionData.name;
if (exceptionData.message) { if (exceptionData.message) {
@ -484,11 +456,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText); console.error(errorText);
} }
onDataUpdated() { private onDataUpdated() {
this.cd.detectChanges(); this.cd.detectChanges();
} }
generateSubscription() { private generateSubscription() {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
@ -497,21 +469,23 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: this.device.id, entityId: this.device.id,
entityName: "Gateway", entityName: 'Gateway',
timeseries: [] timeseries: []
}]; }];
this.dataSource.data.forEach(value => { this.dataSource.data.forEach(value => {
subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`}) subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`});
}) });
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo,this.subscriptionOptions, false, true).subscribe(subscription => { this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo, this.subscriptionOptions,
false, true).subscribe(subscription => {
this.subscription = subscription; this.subscription = subscription;
}); });
} }
} }
getErrorsCount(attribute) { getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key; const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data.find(data=>data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`); const connector = this.subscription && this.subscription.data
return (connector && this.activeConnectors.includes(connectorName))? connector.data[0][1]: 'Inactive'; .find(data => data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName)) ? connector.data[0][1] : 'Inactive';
} }
} }

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html

@ -18,43 +18,39 @@
<nav mat-tab-nav-bar [tabPanel]="tabPanel"> <nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link *ngFor="let link of logLinks" <a mat-tab-link *ngFor="let link of logLinks"
(click)="onTabChanged(link)" (click)="onTabChanged(link)"
[active]="activeLink.name == link.name"> {{link.name}} </a> [active]="activeLink.name === link.name"> {{ link.name }} </a>
</nav> </nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> <mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="ts"> <ng-container matColumnDef="ts">
<mat-header-cell *matHeaderCellDef mat-sort-header> Created time</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss'}} {{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header> Status</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.status' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span> <span [class]="statusClass(attribute.status)">{{ attribute.status }}</span>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="message"> <ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute" [ngClass]="statusClassMsg(attribute.status )"> <mat-cell *matCellDef="let attribute" [class]="statusClassMsg(attribute.status)">
{{ attribute.message }} {{ attribute.message }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-row class="mat-row-select" *matRowDef="let attribute; columns: displayedColumns;"></mat-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let attribute; columns: displayedColumns;"></mat-row>
</table> </table>
<span [fxShow]="dataSource.data.length === 0" <span [fxShow]="dataSource.data.length === 0"
fxFlex fxFlex fxLayoutAlign="center center"
fxLayoutAlign="center center" class="no-data-found">{{ 'attribute.no-telemetry-text' | translate }}</span>
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
<span fxFlex [fxShow]="dataSource.data.length !== 0"></span> <span fxFlex [fxShow]="dataSource.data.length !== 0"></span>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<mat-paginator #paginator <mat-paginator [length]="dataSource.data.length"
[length]="dataSource.data.length"
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> [pageSizeOptions]="[10, 20, 30]"></mat-paginator>

155
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts

@ -14,55 +14,27 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { MatDialogRef } from '@angular/material/dialog';
import { AppState } from '@core/core.state'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { AttributeData, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from "@home/models/datasource/attribute-datasource"; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { Direction, SortOrder } from "@shared/models/page/sort-order";
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { GatewayLogData, GatewayStatus, LogLink } from './gateway-widget.models';
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export interface LogLink {
name: string;
key: string;
filterFn?: Function;
}
@Component({ @Component({
selector: 'tb-gateway-logs', selector: 'tb-gateway-logs',
templateUrl: './gateway-logs.component.html', templateUrl: './gateway-logs.component.html',
styleUrls: ['./gateway-logs.component.scss'] styleUrls: ['./gateway-logs.component.scss']
}) })
export class GatewayLogsComponent extends PageComponent implements AfterViewInit { export class GatewayLogsComponent implements AfterViewInit {
pageLink: PageLink; pageLink: PageLink;
attributeDataSource: AttributeDatasource; dataSource: MatTableDataSource<GatewayLogData>;
dataSource: MatTableDataSource<any>
displayedColumns = ['ts', 'status', 'message']; displayedColumns = ['ts', 'status', 'message'];
@ -76,63 +48,39 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean; textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
logLinks: Array<LogLink>; logLinks: Array<LogLink>;
initialConnector: GatewayConnector;
activeLink: LogLink; activeLink: LogLink;
gatewayLogLinks: Array<LogLink> = [ gatewayLogLinks: Array<LogLink> = [
{ {
name: "General", name: 'General',
key: "LOGS" key: 'LOGS'
}, { }, {
name: "Service", name: 'Service',
key: "SERVICE_LOGS" key: 'SERVICE_LOGS'
}, },
{ {
name: "Connection", name: 'Connection',
key: "CONNECTION_LOGS" key: 'CONNECTION_LOGS'
}, { }, {
name: "Storage", name: 'Storage',
key: "STORAGE_LOGS" key: 'STORAGE_LOGS'
}, },
{ {
key: 'EXTENSIONS_LOGS', key: 'EXTENSIONS_LOGS',
name: "Extension" name: 'Extension'
}] }];
constructor(protected router: Router, constructor() {
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC}; const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC};
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new MatTableDataSource<AttributeData>([]); this.dataSource = new MatTableDataSource<GatewayLogData>([]);
} }
ngAfterViewInit() { ngAfterViewInit() {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
@ -140,22 +88,18 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
this.ctx.defaultSubscription.options.timeWindowConfig = timewindow; this.ctx.defaultSubscription.options.timeWindowConfig = timewindow;
this.ctx.defaultSubscription.updateDataSubscriptions(); this.ctx.defaultSubscription.updateDataSubscriptions();
return timewindow; return timewindow;
} };
if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) { if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) {
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState]; const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];
this.logLinks = [{ this.logLinks = [{
key: `${connector.key}_LOGS`, key: `${connector.key}_LOGS`,
name: "Connector", name: 'Connector',
filterFn: (attrData)=>{ filterFn: (attrData) => !attrData.message.includes(`_converter.py`)
return !attrData.message.includes(`_converter.py`) }, {
}
},{
key: `${connector.key}_LOGS`, key: `${connector.key}_LOGS`,
name: "Converter", name: 'Converter',
filterFn: (attrData)=>{ filterFn: (attrData) => attrData.message.includes(`_converter.py`)
return attrData.message.includes(`_converter.py`) }];
}
}]
} else { } else {
this.logLinks = this.gatewayLogLinks; this.logLinks = this.gatewayLogLinks;
} }
@ -164,20 +108,20 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
} }
updateData(sort?) { private updateData() {
if (this.ctx.defaultSubscription.data.length && this.ctx.defaultSubscription.data[0]) { if (this.ctx.defaultSubscription.data.length && this.ctx.defaultSubscription.data[0]) {
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => { let attrData = this.ctx.defaultSubscription.data[0].data.map(data => {
let result = { const result = {
ts: data[0], ts: data[0],
key: this.activeLink.key, key: this.activeLink.key,
message: /\[(.*)/.exec(data[1])[0], message: /\[(.*)/.exec(data[1])[0],
status: 'INVALID LOG FORMAT' status: 'INVALID LOG FORMAT' as GatewayStatus
}; };
try { try {
result.status= data[1].match(/\|(\w+)\|/)[1]; result.status = data[1].match(/\|(\w+)\|/)[1];
} catch (e) { } catch (e) {
result.status = 'INVALID LOG FORMAT' result.status = 'INVALID LOG FORMAT' as GatewayStatus;
} }
return result; return result;
@ -186,39 +130,35 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
attrData = attrData.filter(data => this.activeLink.filterFn(data)); attrData = attrData.filter(data => this.activeLink.filterFn(data));
} }
this.dataSource.data = attrData; this.dataSource.data = attrData;
if (sort) {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
} }
} }
onTabChanged(link) { onTabChanged(link: LogLink) {
this.activeLink = link; this.activeLink = link;
this.changeSubscription(); this.changeSubscription();
} }
statusClass(status) { statusClass(status: GatewayStatus): string {
switch (status) { switch (status) {
case GatewayLogLevel.debug: case GatewayStatus.DEBUG:
return "status status-debug"; return 'status status-debug';
case GatewayLogLevel.warning: case GatewayStatus.WARNING:
return "status status-warning"; return 'status status-warning';
case GatewayLogLevel.error: case GatewayStatus.ERROR:
case "EXCEPTION": case GatewayStatus.EXCEPTION:
return "status status-error"; return 'status status-error';
case GatewayLogLevel.info:
default: default:
return "status status-info"; return 'status status-info';
} }
} }
statusClassMsg(status) { statusClassMsg(status?: GatewayStatus): string {
if (status === "EXCEPTION") { if (status === GatewayStatus.EXCEPTION) {
return 'msg-status-exception'; return 'msg-status-exception';
} }
} }
changeSubscription() { private changeSubscription() {
if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) { if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) {
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{ this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
name: this.activeLink.key, name: this.activeLink.key,
@ -229,8 +169,7 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
this.ctx.defaultSubscription.updateDataSubscriptions(); this.ctx.defaultSubscription.updateDataSubscriptions();
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => { this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
this.updateData(); this.updateData();
} };
} }
} }

75
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html

@ -15,43 +15,38 @@
limitations under the License. limitations under the License.
--> -->
<form style="width: 600px; position: relative;"> <mat-toolbar color="warn">
<mat-toolbar color="warn"> <mat-icon>warning</mat-icon>
<mat-icon>warning</mat-icon> <h2 translate>gateway.configuration-delete-dialog-header</h2>
<h2 translate>gateway.configuration-delete-dialog-header</h2> <span fxFlex></span>
<span fxFlex></span> <button mat-icon-button
<button mat-icon-button (click)="close()"
(click)="close()" type="button">
type="button"> <mat-icon class="material-icons">close</mat-icon>
<mat-icon class="material-icons">close</mat-icon> </button>
</button> </mat-toolbar>
</mat-toolbar> <div mat-dialog-content style="max-width: 600px" class="mat-content" fxLayout="column">
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> <span innerHTML="{{ 'gateway.configuration-delete-dialog-body' | translate }} <b>{{ gatewayName }}</b>" ></span>
<div class="mat-content" fxLayout="column"> <mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}} <b>{{gatewayName}}</b>" > </span> <mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0"> <input matInput [formControl]="gatewayControl" required/>
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label> <mat-error
<input matInput [formControl]="gatewayControl" required/> *ngIf="gatewayControl.hasError('required')">
<mat-error {{ 'gateway.configuration-delete-dialog-input-required' | translate }}
*ngIf="gatewayControl.hasError('required')"> </mat-error>
{{'gateway.configuration-delete-dialog-input-required' | translate }} </mat-form-field>
</mat-error> </div>
</mat-form-field> <div mat-dialog-actions fxLayoutAlign="end center">
</div> <button mat-button color="warn"
</div> type="button"
<div mat-dialog-actions fxLayoutAlign="end center"> cdkFocusInitial
<button mat-button color="warn" (click)="close()">
type="button" {{ 'action.cancel' | translate }}
cdkFocusInitial </button>
(click)="close()"> <button mat-button color="warn"
{{ 'action.cancel' | translate }} type="button"
</button> [disabled]="gatewayControl.value !== gatewayName"
<button mat-button color="warn" (click)="turnOff()">
type="button" {{ 'gateway.configuration-delete-dialog-confirm' | translate }}
cdkFocusInitial </button>
[disabled]="gatewayControl.value !== gatewayName" </div>
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>
</form>

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -31,11 +31,10 @@ export interface GatewayRemoteConfigurationDialogData {
templateUrl: './gateway-remote-configuration-dialog.html' templateUrl: './gateway-remote-configuration-dialog.html'
}) })
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent, export class GatewayRemoteConfigurationDialogComponent extends
boolean> implements OnInit { DialogComponent<GatewayRemoteConfigurationDialogComponent, boolean>{
gatewayName: string; gatewayName: string;
gatewayControl: FormControl; gatewayControl: FormControl;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -45,10 +44,7 @@ export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<G
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.gatewayName = this.data.gatewayName; this.gatewayName = this.data.gatewayName;
this.gatewayControl = this.fb.control(null); this.gatewayControl = this.fb.control('');
}
ngOnInit(): void {
} }
close(): void { close(): void {

74
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html

@ -16,38 +16,48 @@
--> -->
<div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm"> <div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm">
<mat-form-field class="mat-block tb-value-type"> <ng-container *ngIf="!isConnector; else connectorForm">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label> <mat-form-field>
<mat-select formControlName="command" *ngIf="!isConnector"> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-option *ngFor="let command of RPCCommands" [value]="command"> <mat-select formControlName="command">
{{command}} <mat-option *ngFor="let command of RPCCommands" [value]="command">
</mat-option> {{ command }}
</mat-select> </mat-option>
<input matInput formControlName="command" *ngIf="isConnector"/> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="!isConnector"> <mat-form-field fxFlex>
<mat-label>{{'gateway.statistics.timeout-ms' | translate}}</mat-label> <mat-label>{{ 'gateway.statistics.timeout-ms' | translate }}</mat-label>
<input matInput formControlName="time" type="number" min="1"/> <input matInput formControlName="time" type="number" min="1"/>
<mat-error <mat-error *ngIf="commandForm.get('time').hasError('min')">
*ngIf="commandForm.get('time').hasError('min')"> {{ 'gateway.statistics.timeout-min' | translate }}
{{'gateway.statistics.timeout-min' | translate }} </mat-error>
</mat-error> </mat-form-field>
</mat-form-field> </ng-container>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="isConnector"> <ng-template #connectorForm>
<mat-label>{{'widget-config.datasource-parameters' | translate}}</mat-label> <mat-form-field>
<input matInput formControlName="params" type="JSON"/> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon" <input matInput formControlName="command" />
matSuffix style="cursor:pointer;" </mat-form-field>
(click)="openEditJSONDialog($event)" <mat-form-field fxFlex>
matTooltip="{{'gateway.rpc-command-edit-params' | translate}}">edit <mat-label>{{ 'widget-config.datasource-parameters' | translate }}</mat-label>
</mat-icon> <input matInput formControlName="params" type="JSON"/>
</mat-form-field> <mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
<button mat-raised-button color="primary" (click)="sendCommand()" matIconSuffix style="cursor:pointer;"
[disabled]="commandForm.invalid">{{'gateway.rpc-command-send' | translate}}</button> (click)="openEditJSONDialog($event)"
matTooltip="{{ 'gateway.rpc-command-edit-params' | translate }}">edit
</mat-icon>
</mat-form-field>
</ng-template>
<button mat-raised-button
color="primary"
(click)="sendCommand()"
[disabled]="commandForm.invalid">
{{ 'gateway.rpc-command-send' | translate }}
</button>
</div> </div>
<mat-card class="result-block" [formGroup]="commandForm" fxFlex> <section class="result-block" [formGroup]="commandForm">
<span>{{'gateway.rpc-command-result' | translate}}</span> <span>{{ 'gateway.rpc-command-result' | translate }}</span>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result" fxFlex></tb-json-content> <tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</mat-card> </section>

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss

@ -22,24 +22,24 @@
padding: 0; padding: 0;
.command-form { .command-form {
width: 100%;
flex-wrap: nowrap; flex-wrap: nowrap;
padding: 0 15px 5px; padding: 0 5px 5px;
margin-bottom: 5px;
& > button { & > button {
margin-top: 10px; margin-top: 10px;
} }
} }
.result-block { .result-block {
padding: 0 15px; padding: 0 5px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
& > span { & > span {
font-weight: 600; font-weight: 600;
} }
tb-json-content {
flex: 1;
}
} }
} }
@ -47,9 +47,5 @@
.tb-json-content { .tb-json-content {
height: 100%; height: 100%;
} }
.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
} }

69
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -14,17 +14,9 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, Input } from '@angular/core'; import { AfterViewInit, Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { PageComponent } from "@shared/components/page.component";
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants'; import { ContentType } from '@shared/models/constants';
import { import {
@ -32,13 +24,12 @@ import {
JsonObjectEditDialogData JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
@Component({ @Component({
selector: 'tb-gateway-service-rpc', selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html', templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss'] styleUrls: ['./gateway-service-rpc.component.scss']
}) })
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit { export class GatewayServiceRPCComponent implements AfterViewInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -52,39 +43,28 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
isConnector: boolean; isConnector: boolean;
connectorType: string;
RPCCommands: Array<string> = [ RPCCommands: Array<string> = [
"Ping", 'Ping',
"Stats", 'Stats',
"Devices", 'Devices',
"Update", 'Update',
"Version", 'Version',
"Restart", 'Restart',
"Reboot" 'Reboot'
] ];
private connectorType: string;
constructor(protected router: Router,
protected store: Store<AppState>, constructor(private fb: FormBuilder,
protected fb: FormBuilder, private dialog: MatDialog) {
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
this.commandForm = this.fb.group({ this.commandForm = this.fb.group({
command: [null,[Validators.required]], command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]], time: [60, [Validators.required, Validators.min(1)]],
params: ["{}", [Validators.required]], params: ['{}', [Validators.required]],
result: [null] result: [null]
}) });
} }
ngAfterViewInit() { ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector; this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) { if (!this.isConnector) {
@ -94,16 +74,16 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
} }
} }
sendCommand() { sendCommand() {
const formValues = this.commandForm.value; const formValues = this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_'; const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{ this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe({
this.commandForm.get('result').setValue(JSON.stringify(resp)); next: resp => this.commandForm.get('result').setValue(JSON.stringify(resp)),
},error => { error: error => {
console.log(error); console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error)); this.commandForm.get('result').setValue(JSON.stringify(error.error));
}) }
});
} }
openEditJSONDialog($event: Event) { openEditJSONDialog($event: Event) {
@ -124,5 +104,4 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
} }
); );
} }
} }

26
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html

@ -19,13 +19,13 @@
<div class="statistics-container" fxLayout="row" fxLayout.lt-md="column"> <div class="statistics-container" fxLayout="row" fxLayout.lt-md="column">
<mat-card [formGroup]="statisticForm" *ngIf="!general"> <mat-card [formGroup]="statisticForm" *ngIf="!general">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label>{{'gateway.statistics.statistic' | translate}}</mat-label> <mat-label>{{ 'gateway.statistics.statistic' | translate }}</mat-label>
<mat-select formControlName="statisticKey"> <mat-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key"> <mat-option *ngFor="let key of statisticsKeys" [value]="key">
{{key}} {{ key }}
</mat-option> </mat-option>
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway"> <mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway">
{{command.attributeOnGateway}} {{ command.attributeOnGateway }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -34,44 +34,42 @@
{{'gateway.statistics.statistic-commands-empty' | translate }} {{'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error> </mat-error>
<mat-form-field class="mat-block" *ngIf="commandObj"> <mat-form-field class="mat-block" *ngIf="commandObj">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput [value]="commandObj.command" disabled> <input matInput [value]="commandObj.command" disabled>
</mat-form-field> </mat-form-field>
</mat-card> </mat-card>
<div class="chart-box" fxLayout="column"> <div class="chart-box" fxLayout="column">
<div class="chart-container" #statisticChart [fxShow]="isNumericData"> <div class="chart-container" #statisticChart [fxShow]="isNumericData"></div>
</div>
<table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource" <table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="0"> <ng-container matColumnDef="0">
<mat-header-cell *matHeaderCellDef mat-sort-header>{{'audit-log.timestamp' | translate}}</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"> <mat-cell *matCellDef="let row; let rowIndex = index">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }} {{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="1"> <ng-container matColumnDef="1">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{"event.message" | translate}}</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
{{ row[1] }} {{ row[1] }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}" <mat-row class="mat-row-select"
*matRowDef="let row; columns: displayedColumns;"></mat-row> *matRowDef="let row; columns: displayedColumns;"></mat-row>
</table> </table>
<span [fxShow]="dataSource.data.length === 0 && !isNumericData" <span [fxShow]="dataSource.data.length === 0 && !isNumericData"
fxLayoutAlign="center center" fxLayoutAlign="center center"
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span> class="no-data-found">{{ 'attribute.no-telemetry-text' | translate }}</span>
<div fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData"> <div fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData">
<div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row" <div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row"
fxLayoutAlign="center center"> fxLayoutAlign="center center">
<span class="legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span> <span class="legend-line" [style.background-color]="legendKey.dataKey.color"></span>
<div class="legend-label" <div class="legend-label"
(click)="onLegendKeyHiddenChange(legendKey.dataIndex)" (click)="onLegendKeyHiddenChange(legendKey.dataIndex)"
[ngClass]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }" [class]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }"
[innerHTML]="legendKey.dataKey.label"> [innerHTML]="legendKey.dataKey.label">
</div> </div>
</div> </div>

119
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts

@ -14,18 +14,10 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget'; import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
@ -41,13 +33,12 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({ @Component({
selector: 'tb-gateway-statistics', selector: 'tb-gateway-statistics',
templateUrl: './gateway-statistics.component.html', templateUrl: './gateway-statistics.component.html',
styleUrls: ['./gateway-statistics.component.scss'] styleUrls: ['./gateway-statistics.component.scss']
}) })
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit { export class GatewayStatisticsComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ViewChild('statisticChart') statisticChart: ElementRef; @ViewChild('statisticChart') statisticChart: ElementRef;
@ -58,10 +49,10 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
@Input() @Input()
public general: boolean; public general: boolean;
public isNumericData: boolean = false; public isNumericData = false;
public chartInited: boolean; public chartInited: boolean;
private flot: TbFlot; private flot: TbFlot;
private flotCtx; private flotCtx: WidgetContext;
public statisticForm: FormGroup; public statisticForm: FormGroup;
public statisticsKeys = []; public statisticsKeys = [];
public commands = []; public commands = [];
@ -89,70 +80,61 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
}; };
constructor(protected router: Router, constructor(private fb: FormBuilder,
protected store: Store<AppState>, private attributeService: AttributeService,
protected fb: FormBuilder, private utils: UtilsService) {
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private cd: ChangeDetectorRef,
private utils: UtilsService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC}; const sortOrder: SortOrder = {property: '0', direction: Direction.DESC};
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder); this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
this.displayedColumns = ['0', '1']; this.displayedColumns = ['0', '1'];
this.dataSource = new MatTableDataSource<any>([]); this.dataSource = new MatTableDataSource<any>([]);
this.statisticForm = this.fb.group({ this.statisticForm = this.fb.group({
statisticKey: [null, []] statisticKey: [null, []]
}) });
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => { this.statisticForm.get('statisticKey').valueChanges.subscribe(value => {
this.commandObj = null; this.commandObj = null;
if (this.commands.length) { if (this.commands.length) {
this.commandObj = this.commands.find(command => command.attributeOnGateway === value); this.commandObj = this.commands.find(command => command.attributeOnGateway === value);
} }
if (this.subscriptionInfo) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value); if (this.subscriptionInfo) {
}) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value);
}
});
} }
ngAfterViewInit() { ngAfterViewInit() {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.sort.sortChange.subscribe(_=>{ this.sort.sortChange.subscribe(() => this.sortData());
this.sortData();
})
this.init(); this.init();
if (this.ctx.defaultSubscription.datasources.length) { if (this.ctx.defaultSubscription.datasources.length) {
const gateway = this.ctx.defaultSubscription.datasources[0].entity; const gateway = this.ctx.defaultSubscription.datasources[0].entity;
if (gateway.id.id === NULL_UUID) return; if (gateway.id.id === NULL_UUID) {
return;
}
if (!this.general) { if (!this.general) {
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => { this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ['general_configuration'])
if (resp && resp.length) { .subscribe((resp: AttributeData[]) => {
this.commands = resp[0].value.statistics.commands; if (resp && resp.length) {
if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) { this.commands = resp[0].value.statistics.commands;
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway); if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) {
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway); this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
}
} }
} });
})
} else { } else {
let connectorsTs;
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe( this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe(
data => { data => {
connectorsTs = Object.keys(data) const connectorsTs = Object.keys(data)
.filter(el => el.includes( .filter(el => el.includes('ConnectorEventsProduced') || el.includes('ConnectorEventsSent'));
'ConnectorEventsProduced'
) || el.includes(
'ConnectorEventsSent'))
this.createGeneralChartsSubscription(gateway, connectorsTs); this.createGeneralChartsSubscription(gateway, connectorsTs);
}) });
} }
} }
} }
public sortData () { public sortData() {
this.dataSource.sortData(this.dataSource.data, this.sort); this.dataSource.sortData(this.dataSource.data, this.sort);
} }
@ -162,7 +144,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
} }
private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) { private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) {
let subscriptionInfo = [{ const subscriptionInfo = [{
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: gateway.id.id, entityId: gateway.id.id,
@ -175,8 +157,8 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
this.changeSubscription(subscriptionInfo); this.changeSubscription(subscriptionInfo);
} }
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) { private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: string[]) {
let subscriptionInfo = [{ const subscriptionInfo = [{
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: gateway.id.id, entityId: gateway.id.id,
@ -184,20 +166,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
timeseries: [] timeseries: []
}]; }];
subscriptionInfo[0].timeseries = []; subscriptionInfo[0].timeseries = [];
if (attrData && attrData.length) { if (attrData?.length) {
attrData.forEach(attr => { attrData.forEach(attr => {
subscriptionInfo[0].timeseries.push({name: attr, label: attr}) subscriptionInfo[0].timeseries.push({name: attr, label: attr});
}) });
} }
this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => { this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => {
subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label}) subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label});
}) });
this.subscriptionInfo = subscriptionInfo; this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo); this.changeSubscription(subscriptionInfo);
} }
init = () => { private init = () => {
this.flotCtx = { this.flotCtx = {
$scope: this.ctx.$scope, $scope: this.ctx.$scope,
$injector: this.ctx.$injector, $injector: this.ctx.$injector,
@ -207,20 +189,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionApi: this.ctx.subscriptionApi, subscriptionApi: this.ctx.subscriptionApi,
detectChanges: this.ctx.detectChanges, detectChanges: this.ctx.detectChanges,
settings: this.ctx.settings settings: this.ctx.settings
}; } as WidgetContext;
} };
updateChart = () => { private updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) { if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update(); this.flot.update();
} }
} };
resize = () => { private resize = () => {
if (this.flot) { if (this.flot) {
this.flot.resize(); this.flot.resize();
} }
} };
private reset() { private reset() {
if (this.resize$) { if (this.resize$) {
@ -260,7 +242,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
this.chartInited = true; this.chartInited = true;
this.flotCtx.$container = $(this.statisticChart.nativeElement); this.flotCtx.$container = $(this.statisticChart.nativeElement);
this.resize$.observe(this.statisticChart.nativeElement); this.resize$.observe(this.statisticChart.nativeElement);
this.flot = new TbFlot(this.flotCtx as WidgetContext, "line"); this.flot = new TbFlot(this.flotCtx as WidgetContext, 'line');
this.flot.update(); this.flot.update();
} }
@ -270,16 +252,17 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
return; return;
} }
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : []; this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : [];
this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]) ); this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]));
} }
changeSubscription(subscriptionInfo: SubscriptionInfo[]) { private changeSubscription(subscriptionInfo: SubscriptionInfo[]) {
if (this.subscription) { if (this.subscription) {
this.reset(); this.reset();
} }
if (this.ctx.datasources[0].entity) { if (this.ctx.datasources[0].entity) {
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions, false, true).subscribe(subscription => { this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions,
false, true).subscribe(subscription => {
this.subscription = subscription; this.subscription = subscription;
this.isDataOnlyNumbers(); this.isDataOnlyNumbers();
this.legendData = this.subscription.legendData; this.legendData = this.subscription.legendData;
@ -291,9 +274,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
if (this.isNumericData) { if (this.isNumericData) {
this.initChart(); this.initChart();
} }
}) });
} }
} }
} }

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

@ -0,0 +1,142 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
SQLITE = 'sqlite'
}
export enum DeviceGatewayStatus {
EXCEPTION = 'EXCEPTION'
}
export enum GatewayLogLevel {
NONE = 'NONE',
CRITICAL = 'CRITICAL',
ERROR = 'ERROR',
WARNING = 'WARNING',
INFO = 'INFO',
DEBUG = 'DEBUG'
}
export const GatewayStatus = {
...GatewayLogLevel,
...DeviceGatewayStatus
};
export type GatewayStatus = DeviceGatewayStatus | GatewayLogLevel;
export enum LogSavingPeriod {
days = 'D',
hours = 'H',
minutes = 'M',
seconds = 'S'
}
export enum LocalLogsConfigs {
service = 'service',
connector = 'connector',
converter = 'converter',
tb_connection = 'tb_connection',
storage = 'storage',
extension = 'extension'
}
export const LocalLogsConfigTranslateMap = new Map<LocalLogsConfigs, string>([
[LocalLogsConfigs.service, 'Service'],
[LocalLogsConfigs.connector, 'Connector'],
[LocalLogsConfigs.converter, 'Converter'],
[LocalLogsConfigs.tb_connection, 'TB Connection'],
[LocalLogsConfigs.storage, 'Storage'],
[LocalLogsConfigs.extension, 'Extension']
]);
export const LogSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[
[LogSavingPeriod.days, 'gateway.logs.days'],
[LogSavingPeriod.hours, 'gateway.logs.hours'],
[LogSavingPeriod.minutes, 'gateway.logs.minutes'],
[LogSavingPeriod.seconds, 'gateway.logs.seconds']
]
);
export const StorageTypesTranslationMap = new Map<StorageTypes, string>(
[
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
[StorageTypes.FILE, 'gateway.storage-types.file-storage'],
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
]
);
export enum SecurityTypes {
ACCESS_TOKEN = 'accessToken',
USERNAME_PASSWORD = 'usernamePassword',
TLS_ACCESS_TOKEN = 'tlsAccessToken',
TLS_PRIVATE_KEY = 'tlsPrivateKey'
}
export const GecurityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
]
);
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([
['mqtt', 'MQTT'],
['modbus', 'MODBUS'],
['grpc', 'GRPC'],
['opcua', 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'],
['ble', 'BLE'],
['request', 'REQUEST'],
['can', 'CAN'],
['bacnet', 'BACNET'],
['odbc', 'ODBC'],
['rest', 'REST'],
['snmp', 'SNMP'],
['ftp', 'FTP'],
['socket', 'SOCKET'],
['xmpp', 'XMPP'],
['ocpp', 'OCCP'],
['custom', 'CUSTOM']
]);
export interface LogLink {
name: string;
key: string;
filterFn?: (arg: any) => boolean;
}
export interface GatewayLogData {
ts: number;
key: string;
message: string;
status: GatewayStatus;
}

19
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html

@ -15,12 +15,13 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column"> <div class="tb-form-panel" [formGroup]="gatewayLogSettingForm">
<mat-slide-toggle class="mat-slide" formControlName="isConnectorLog"> <div class="tb-form-row column-xs">
{{"widgets.gateway.is-connector" | translate}} <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="isConnectorLog">
</mat-slide-toggle> {{"widgets.gateway.is-connector" | translate}}
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value"> </mat-slide-toggle>
<mat-label>{{"widgets.gateway.state-param-name" | translate}}</mat-label> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="connectorLogState"> <input matInput formControlName="connectorLogState" placeholder="{{ 'widgets.gateway.state-param-name' | translate }}">
</mat-form-field> </mat-form-field>
</section> </div>
</div>

27
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts

@ -16,7 +16,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -27,15 +27,15 @@ import { AppState } from '@core/core.state';
}) })
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent { export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup; gatewayLogSettingForm: FormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): FormGroup {
return this.GatewayLogSettingForm; return this.gatewayLogSettingForm;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
@ -46,9 +46,22 @@ export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
} }
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({ this.gatewayLogSettingForm = this.fb.group({
isConnectorLog: [false, []], isConnectorLog: [false, []],
connectorLogState: ['default', []] connectorLogState: ['default', Validators.required]
}); });
} }
protected validatorTriggers(): string[] {
return ['isConnectorLog'];
}
protected updateValidators(emitEvent: boolean) {
const isConnectorLog: boolean = this.gatewayLogSettingForm.get('isConnectorLog').value;
if (isConnectorLog) {
this.gatewayLogSettingForm.get('connectorLogState').enable({emitEvent});
} else {
this.gatewayLogSettingForm.get('connectorLogState').disable({emitEvent});
}
}
} }

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html

@ -15,8 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column"> <div class="tb-form-panel" [formGroup]="gatewayServiceRPCSettingForm">
<mat-slide-toggle class="mat-slide" formControlName="isConnector"> <div class="tb-form-row column-xs">
{{"widgets.gateway.is-connector" | translate}} <mat-slide-toggle class="mat-slide margin" formControlName="isConnector">
</mat-slide-toggle> {{"widgets.gateway.is-connector" | translate}}
</section> </mat-slide-toggle>
</div>
</div>

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts

@ -16,7 +16,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -27,15 +27,15 @@ import { AppState } from '@core/core.state';
}) })
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent { export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup; gatewayServiceRPCSettingForm: FormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): FormGroup {
return this.GatewayLogSettingForm; return this.gatewayServiceRPCSettingForm;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
@ -45,7 +45,7 @@ export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent
} }
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({ this.gatewayServiceRPCSettingForm = this.fb.group({
isConnector: [false, []] isConnector: [false, []]
}); });
} }

14
ui-ngx/src/app/shared/components/file-input.component.html

@ -16,12 +16,14 @@
--> -->
<div class="tb-container"> <div class="tb-container">
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }} <ng-container *ngIf="hint; else title">
<mat-icon *ngIf="hint" class="material-icons-outlined pointer-event" <label class="tb-title pointer-event" [class.tb-required]="!disabled && required"
style="cursor:pointer;" tb-hint-tooltip-icon="{{ hint }}">{{ label }}
matTooltip="{{hint}}">info_outlined </label>
</mat-icon> </ng-container>
</label> <ng-template #title>
<label class="tb-title" [class.tb-required]="!disabled && required">{{ label }}</label>
</ng-template>
<ng-container #flow="flow" <ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}"> [flowConfig]="{allowDuplicateUploads: true}">
<div class="tb-file-select-container"> <div class="tb-file-select-container">

4
ui-ngx/src/app/shared/components/file-input.component.scss

@ -22,8 +22,10 @@ $previewSize: 100px !default;
.tb-container { .tb-container {
margin-top: 0; margin-top: 0;
label.tb-title { label.tb-title {
display: block;
padding-bottom: 8px; padding-bottom: 8px;
&:not(.pointer-event) {
display: block;
}
} }
} }

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

@ -2635,7 +2635,7 @@
"gateway": { "gateway": {
"add-entry": "Add configuration", "add-entry": "Add configuration",
"advanced": "Advanced", "advanced": "Advanced",
"checking-device-activity": "Checking Device Activity:", "checking-device-activity": "Checking device activity",
"command": "Docker commands", "command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard", "command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration", "configuration": "Configuration",
@ -2888,7 +2888,7 @@
"log-format": "Log message format", "log-format": "Log message format",
"remote-log": "Enables remote logging and logs reading from the gateway", "remote-log": "Enables remote logging and logs reading from the gateway",
"backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted", "backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the ThingsBoard platform", "storage": "Provides configuration for saving incoming data before it is sent to the platform",
"file": "Received data saving to the hard drive", "file": "Received data saving to the hard drive",
"memory": "Received data saving to the RAM memory", "memory": "Received data saving to the RAM memory",
"sqlite": "Received data saving to the .db file", "sqlite": "Received data saving to the .db file",
@ -5330,7 +5330,10 @@
"events-filter": "Events filter", "events-filter": "Events filter",
"event-key-contains": "Event key contains...", "event-key-contains": "Event key contains...",
"is-connector": "Is Connector", "is-connector": "Is Connector",
"state-param-name": "State parameter connector name" "state-param-name": "State parameter connector name",
"status": "Status",
"message": "Message",
"created-time": "Created time"
}, },
"gauge": { "gauge": {
"default-color": "Default color", "default-color": "Default color",

5
ui-ngx/src/form.scss

@ -65,6 +65,9 @@
box-shadow: none; box-shadow: none;
border-radius: 0; border-radius: 0;
} }
&.no-gap {
gap: 0;
}
&.tb-slide-toggle { &.tb-slide-toggle {
padding: 0; padding: 0;
gap: 0; gap: 0;
@ -218,7 +221,7 @@
} }
} }
.tb-form-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field { .tb-form-row:not(.tb-not-inline-field) .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field {
&.mat-form-field-appearance-fill { &.mat-form-field-appearance-fill {
.mdc-text-field--filled:not(.mdc-text-field--disabled) { .mdc-text-field--filled:not(.mdc-text-field--disabled) {
&:before { &:before {

Loading…
Cancel
Save