Browse Source

UI: Add device connectivity dialog; improve work ngrx store from user settings

pull/8938/head
Vladyslav_Prykhodko 3 years ago
parent
commit
038ab25403
  1. 19
      ui-ngx/src/app/core/auth/auth.actions.ts
  2. 14
      ui-ngx/src/app/core/auth/auth.effects.ts
  3. 15
      ui-ngx/src/app/core/auth/auth.reducer.ts
  4. 6
      ui-ngx/src/app/core/auth/auth.selectors.ts
  5. 4
      ui-ngx/src/app/core/http/device.service.ts
  6. 2
      ui-ngx/src/app/core/utils.ts
  7. 9
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  8. 226
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html
  9. 151
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss
  10. 170
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts
  11. 6
      ui-ngx/src/app/modules/home/pages/device/device.component.html
  12. 4
      ui-ngx/src/app/modules/home/pages/device/device.module.ts
  13. 35
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  14. 2
      ui-ngx/src/app/shared/components/markdown.component.scss
  15. 16
      ui-ngx/src/app/shared/models/device.models.ts
  16. 1
      ui-ngx/src/app/shared/models/user-settings.models.ts
  17. 40
      ui-ngx/src/assets/help/en_US/device/install_coap_client.md
  18. 34
      ui-ngx/src/assets/help/en_US/device/install_curl.md
  19. 38
      ui-ngx/src/assets/help/en_US/device/install_mqtt_client.md
  20. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  21. 49
      ui-ngx/src/form.scss
  22. 21
      ui-ngx/src/typings/utils.d.ts

19
ui-ngx/src/app/core/auth/auth.actions.ts

@ -17,6 +17,7 @@
import { Action } from '@ngrx/store';
import { User } from '@shared/models/user.model';
import { AuthPayload } from '@core/auth/auth.models';
import { UserSettings } from '@shared/models/user-settings.models';
export enum AuthActionTypes {
AUTHENTICATED = '[Auth] Authenticated',
@ -25,7 +26,9 @@ export enum AuthActionTypes {
UPDATE_USER_DETAILS = '[Auth] Update User Details',
UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id',
UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository',
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section'
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section',
UPDATE_USER_SETTINGS = '[Preferences] Update user settings',
DELETE_USER_SETTINGS = '[Preferences] Delete user settings',
}
export class ActionAuthAuthenticated implements Action {
@ -68,6 +71,18 @@ export class ActionPreferencesUpdateOpenedMenuSection implements Action {
constructor(readonly payload: { path: string; opened: boolean }) {}
}
export class ActionPreferencesUpdateUserSettings implements Action {
readonly type = AuthActionTypes.UPDATE_USER_SETTINGS;
constructor(readonly payload: Partial<UserSettings>) {}
}
export class ActionPreferencesDeleteUserSettings implements Action {
readonly type = AuthActionTypes.DELETE_USER_SETTINGS;
constructor(readonly payload: Array<NestedKeyOf<UserSettings>>) {}
}
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository |
ActionPreferencesUpdateOpenedMenuSection;
ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesUpdateUserSettings | ActionPreferencesDeleteUserSettings;

14
ui-ngx/src/app/core/auth/auth.effects.ts

@ -39,4 +39,18 @@ export class AuthEffects {
withLatestFrom(this.store.pipe(select(selectAuthState))),
mergeMap(([action, state]) => this.userSettingsService.putUserSettings({ openedMenuSections: state.userSettings.openedMenuSections }))
), {dispatch: false});
updatedUserSettings = createEffect(() => this.actions$.pipe(
ofType(
AuthActionTypes.UPDATE_USER_SETTINGS,
),
mergeMap((state) => this.userSettingsService.putUserSettings(state.payload))
), {dispatch: false});
deleteUserSettings = createEffect(() => this.actions$.pipe(
ofType(
AuthActionTypes.DELETE_USER_SETTINGS,
),
mergeMap((state) => this.userSettingsService.deleteUserSettings(state.payload))
), {dispatch: false});
}

15
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -16,7 +16,8 @@
import { AuthPayload, AuthState } from './auth.models';
import { AuthActions, AuthActionTypes } from './auth.actions';
import { initialUserSettings } from '@shared/models/user-settings.models';
import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models';
import { unset } from '@core/utils';
const emptyUserAuthState: AuthPayload = {
authUser: null,
@ -42,6 +43,7 @@ export const authReducer = (
state: AuthState = initialState,
action: AuthActions
): AuthState => {
let userSettings: UserSettings;
switch (action.type) {
case AuthActionTypes.AUTHENTICATED:
return { ...state, isAuthenticated: true, ...action.payload };
@ -71,7 +73,16 @@ export const authReducer = (
} else {
openedMenuSections.delete(action.payload.path);
}
const userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}};
userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}};
return { ...state, ...{ userSettings }};
case AuthActionTypes.UPDATE_USER_SETTINGS:
userSettings = {...state.userSettings, ...action.payload};
return { ...state, ...{ userSettings }};
case AuthActionTypes.DELETE_USER_SETTINGS:
userSettings = {...state.userSettings};
action.payload.forEach(path => unset(userSettings, path));
return { ...state, ...{ userSettings }};
default:

6
ui-ngx/src/app/core/auth/auth.selectors.ts

@ -21,6 +21,7 @@ import { AuthState } from './auth.models';
import { take } from 'rxjs/operators';
import { AuthUser } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
import { getDescendantProp } from '@core/utils';
export const selectAuthState = createFeatureSelector< AuthState>(
'auth'
@ -76,6 +77,11 @@ export const selectUserSettings = createSelector(
(state: AuthState) => state.userSettings
);
export const selectUserSettingsProperty = (path: NestedKeyOf<UserSettings>) => createSelector(
selectAuthState,
(state: AuthState) => getDescendantProp(state.userSettings, path)
);
export const selectOpenedMenuSections = createSelector(
selectAuthState,
(state: AuthState) => state.userSettings.openedMenuSections

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

@ -208,4 +208,8 @@ export class DeviceService {
return this.http.post<BulkImportResult>('/api/device/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
}
public getDevicePublishTelemetryCommands(deviceId: string, config?: RequestConfig): Observable<{[key: string]: string}> {
return this.http.get<{[key: string]: string}>(`/api/device/${deviceId}/commands`, defaultHttpOptionsFromConfig(config));
}
}

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

@ -315,6 +315,8 @@ export const isEqual = (a: any, b: any): boolean => _.isEqual(a, b);
export const isEmpty = (a: any): boolean => _.isEmpty(a);
export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path);
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
if (a === b) {
return true;

9
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts

@ -23,7 +23,6 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { Device, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
import { MatStepper, StepperOrientation } from '@angular/material/stepper';
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@ -40,7 +39,7 @@ import { HttpErrorResponse } from '@angular/common/http';
templateUrl: './device-wizard-dialog.component.html',
styleUrls: ['./device-wizard-dialog.component.scss']
})
export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDialogComponent, boolean> {
export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDialogComponent, Device> {
@ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper;
@ -64,7 +63,7 @@ export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDia
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, Device>,
private deviceService: DeviceService,
private breakpointObserver: BreakpointObserver,
private fb: FormBuilder) {
@ -121,7 +120,7 @@ export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDia
add(): void {
if (this.allValid()) {
this.createDevice().subscribe(
() => this.dialogRef.close(true)
(device) => this.dialogRef.close(device)
);
}
}
@ -137,7 +136,7 @@ export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDia
}
}
private createDevice(): Observable<BaseData<HasId>> {
private createDevice(): Observable<Device> {
const device: Device = {
name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value,

226
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html

@ -0,0 +1,226 @@
<!--
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.
-->
<mat-toolbar color="primary">
<h2 translate>device.connectivity.check-connectivity</h2>
<span fxFlex></span>
<!-- <div [tb-help]="'check-connectivity'"></div>-->
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<section *ngIf="loadedCommand; else loadingCommand" class="tb-form-panel no-padding no-border">
<tb-toggle-header #transportProtocol
value="{{ selectTransportType }}"
name="TransportProtocol"
useSelectOnMdLg="false"
appearance="fill"
[fxHide]="allowTransportType.size < 2">
<tb-toggle-option
*ngIf="allowTransportType.has(BasicTransportType.HTTP)"
[value]="BasicTransportType.HTTP">
{{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.MQTT)"
[value]="DeviceTransportType.MQTT">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.COAP)"
[value]="DeviceTransportType.COAP">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.SNMP)"
[value]="DeviceTransportType.SNMP">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }}
</tb-toggle-option>
<tb-toggle-option
*ngIf="allowTransportType.has(DeviceTransportType.LWM2M)"
[value]="DeviceTransportType.LWM2M">
{{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }}
</tb-toggle-option>
</tb-toggle-header>
<div class="tb-form-panel stroked" [fxHide]="!allowTransportType.size">
<ng-container [ngSwitch]="transportProtocol.value">
<ng-template [ngSwitchCase]="BasicTransportType.HTTP">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.use-following-instructions</div>
<div class="tb-hint-instruction">
<span class="content tb-form-panel-hint" translate>device.connectivity.install-curl</span>
<span tb-help-popup="device/install_curl"
tb-help-popup-placement="bottom"
trigger-style="letter-spacing:0.25px; font-size: 12px"
[tb-help-popup-style]="{maxWidth: '800px'}"
trigger-text="{{ 'action.see-documentation' | translate }}"></span>
</div>
<div *ngIf="commands.http">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.http-command</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(commands.http)></tb-markdown>
</div>
<div *ngIf="commands.https">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.https-command</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(commands.https)></tb-markdown>
</div>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.MQTT">
<div *ngIf="!(commands.mqtts && commands.mqtts === 'Check documentation')" class="tb-form-panel no-padding no-border">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.use-following-instructions</div>
<div class="tb-hint-instruction">
<span class="content tb-form-panel-hint" translate>device.connectivity.install-mqtt-client</span>
<span tb-help-popup="device/install_mqtt_client"
tb-help-popup-placement="bottom"
trigger-style="letter-spacing:0.25px; font-size: 12px"
[tb-help-popup-style]="{maxWidth: '800px'}"
trigger-text="{{ 'action.see-documentation' | translate }}"></span>
</div>
</div>
<div *ngIf="commands.mqtt">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.mqtt-command</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(commands.mqtt)></tb-markdown>
</div>
<div *ngIf="commands.mqtts">
<div *ngIf="commands.mqtts !== 'Check documentation'; else mqttCertificate">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.mqtts-command</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(commands.mqtts)></tb-markdown>
</div>
<ng-template #mqttCertificate>
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.mqtts-x509-command</div>
<div>
<a mat-button color="primary" href="https://thingsboard.io/docs/user-guide/certificates/?ubuntuThingsboardX509=x509Chain" target="_blank">
<span>
{{ 'action.see-documentation' | translate }}
<mat-icon class="tb-mat-18">open_in_new</mat-icon>
</span>
</a>
</div>
</ng-template>
</div>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.COAP">
<div *ngIf="!(commands.coaps && commands.coaps === 'Check documentation')" class="tb-form-panel no-padding no-border">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.use-following-instructions</div>
<div class="tb-hint-instruction">
<span class="content tb-form-panel-hint" translate>device.connectivity.install-coap-cli</span>
<span tb-help-popup="device/install_coap_client"
tb-help-popup-placement="bottom"
trigger-style="letter-spacing:0.25px; font-size: 12px"
[tb-help-popup-style]="{maxWidth: '800px'}"
trigger-text="{{ 'action.see-documentation' | translate }}"></span>
</div>
</div>
<div *ngIf="commands.coap">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.coap-command</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(commands.coap)></tb-markdown>
</div>
<div *ngIf="commands.coaps">
<div *ngIf="commands.coaps !== 'Check documentation'; else coapCertificate">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.coaps-command</div>
<tb-markdown usePlainMarkdown containerClass="tb-command-code"
[data]=createMarkDownCommand(commands.coaps)></tb-markdown>
</div>
<ng-template #coapCertificate>
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.coaps-x509-command</div>
<div>
<a mat-button color="primary" href="https://thingsboard.io/docs/user-guide/ssl/coap-x509-certificates/" target="_blank">
<span>
{{ 'action.see-documentation' | translate }}
<mat-icon class="tb-mat-18">open_in_new</mat-icon>
</span>
</a>
</div>
</ng-template>
</div>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.SNMP">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.snmp-command</div>
<div>
<a mat-button color="primary" href="https://thingsboard.io/docs/reference/snmp-api/" target="_blank">
<span>
{{ 'action.see-documentation' | translate }}
<mat-icon class="tb-mat-18">open_in_new</mat-icon>
</span>
</a>
</div>
</ng-template>
<ng-template [ngSwitchCase]="DeviceTransportType.LWM2M">
<div class="tb-form-panel-hint tb-font-14" translate>device.connectivity.lwm2m-command</div>
<div>
<a mat-button color="primary" href="https://thingsboard.io/docs/reference/lwm2m-api/" target="_blank">
<span>
{{ 'action.see-documentation' | translate }}
<mat-icon class="tb-mat-18">open_in_new</mat-icon>
</span>
</a>
</div>
</ng-template>
</ng-container>
</div>
<div class="tb-form-panel stroked">
<div fxFlex fxLayout="row">
<div class="tb-form-panel-title" translate>device.state</div>
<div class="status" [ngClass]="{'inactive': !status}">
{{ (status ? 'device.active' : 'device.inactive') | translate }}
</div>
</div>
<div class="tb-form-panel-hint tb-font-14" translate>attribute.latest-telemetry</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell" fxFlex translate>device.time</div>
<div class="tb-form-table-header-cell" fxFlex translate>attribute.key</div>
<div class="tb-form-table-header-cell" fxFlex translate>attribute.value</div>
</div>
<div *ngIf="latestTelemetry.length; else noData" class="tb-form-table-body">
<div class="tb-form-table-row" *ngFor="let telemetry of latestTelemetry">
<div class="tb-form-table-row-cell" fxFlex>{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}</div>
<div class="tb-form-table-row-cell" fxFlex>{{ telemetry.key }}</div>
<div class="tb-form-table-row-cell" fxFlex>{{ telemetry.value }}</div>
</div>
</div>
</div>
</div>
</section>
</div>
<div mat-dialog-actions class="tb-dialog-actions">
<mat-slide-toggle fxShow="{{ showDontShowAgain }}" [(ngModel)]="notShowAgain">{{ 'action.dont-show-again' | translate}}</mat-slide-toggle>
<span fxFlex></span>
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="close()">{{ 'action.cancel' | translate }}</button>
</div>
<ng-template #loadingCommand>
<div class="tb-loader">
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner>
<span class="mat-subtitle-1 label">
{{ 'device.connectivity.loading-check-connectivity-command' | translate }}
</span>
</div>
</ng-template>
<ng-template #noData>
<div class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>attribute.no-latest-telemetry</div>
</div>
</ng-template>

151
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss

@ -0,0 +1,151 @@
/**
* 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.
*/
@import "../../../../../scss/constants";
:host {
height: 100%;
max-height: 100vh;
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.tb-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
height: 300px;
max-height: 100%;
.label {
margin-bottom: 0;
text-align: center;
}
}
.status {
margin-left: 12px;
border-radius: 12px;
height: 24px;
line-height: 24px;
padding: 0 8px;
width: fit-content;
color: #198038;
background-color: rgba(25, 128, 56, 0.08);
font-size: 14px;
&.inactive {
color: #d12730;
background-color: rgba(209, 39, 48, 0.08);
}
}
.tb-hint-instruction {
border-radius: 6px;
background-color: rgba(48, 86, 128, 0.04);
padding: 6px 16px;
.content {
vertical-align: middle;
}
}
.tb-font-14 {
font-size: 14px;
}
.tb-form-table-body {
max-height: 88px;
overflow-y: auto;
scrollbar-gutter: stable;
.tb-form-table-row {
min-height: 38px;
}
}
.tb-no-data-available {
.tb-no-data-bg {
min-height: 68px;
}
}
@media #{$mat-sm} {
width: 470px;
}
@media #{$mat-gt-sm} {
width: 720px;
}
}
:host-context(.mat-mdc-dialog-container) {
.tb-dialog-actions {
display: flex;
gap: 8px;
padding: 8px 16px;
}
.mat-mdc-dialog-content {
max-height: 80vh;
padding: 16px;
}
}
:host ::ng-deep {
.tb-markdown-view {
.tb-command-code {
.code-wrapper {
padding: 0;
pre[class*=language-] {
background: #F3F6FA;
border-color: #305680;
}
}
button.clipboard-btn {
right: 0;
p {
color: #305680;
}
p, div {
background-color: #F3F6FA;
}
div {
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
}
}
}
}
}
.mdc-button__label > span {
.mat-icon {
vertical-align: text-bottom;
box-sizing: initial;
}
}
}

170
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts

@ -0,0 +1,170 @@
///
/// 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.
///
import { Component, Inject, NgZone, OnDestroy, OnInit } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DeviceService } from '@core/http/device.service';
import { FormBuilder } from '@angular/forms';
import {
AttributeData,
AttributeScope,
AttributesSubscriptionCmd,
LatestTelemetry,
TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors';
import { take } from 'rxjs/operators';
import {
BasicTransportType,
DeviceTransportType,
deviceTransportTypeTranslationMap,
NetworkTransportType
} from '@shared/models/device.models';
import { UserSettingsService } from '@core/http/user-settings.service';
import { ActionPreferencesUpdateUserSettings } from '@core/auth/auth.actions';
export interface DeviceCheckConnectivityDialogData {
deviceId: EntityId;
showDontShowAgain: boolean;
}
@Component({
selector: 'tb-device-check-connectivity-dialog',
templateUrl: './device-check-connectivity-dialog.component.html',
styleUrls: ['./device-check-connectivity-dialog.component.scss']
})
export class DeviceCheckConnectivityDialogComponent extends
DialogComponent<DeviceCheckConnectivityDialogComponent> implements OnInit, OnDestroy {
loadedCommand = false;
status: boolean;
latestTelemetry: Array<AttributeData> = [];
commands: {[key: string]: string};
allowTransportType = new Set<NetworkTransportType>();
selectTransportType: NetworkTransportType;
BasicTransportType = BasicTransportType;
DeviceTransportType = DeviceTransportType;
deviceTransportTypeTranslationMap = deviceTransportTypeTranslationMap;
showDontShowAgain = this.data.showDontShowAgain;
notShowAgain = false;
private telemetrySubscriber: TelemetrySubscriber;
private currentTime = Date.now();
private transportTypes = [...Object.keys(BasicTransportType), ...Object.keys(DeviceTransportType)] as Array<NetworkTransportType>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) private data: DeviceCheckConnectivityDialogData,
public dialogRef: MatDialogRef<DeviceCheckConnectivityDialogComponent>,
private fb: FormBuilder,
private deviceService: DeviceService,
private telemetryWsService: TelemetryWebsocketService,
private userSettingsService: UserSettingsService,
private zone: NgZone) {
super(store, router, dialogRef);
}
ngOnInit() {
this.loadCommands();
this.subscribeToLatestTelemetry();
}
ngOnDestroy() {
super.ngOnDestroy();
this.telemetrySubscriber?.complete();
this.telemetrySubscriber?.unsubscribe();
}
close(): void {
if (this.notShowAgain && this.showDontShowAgain) {
this.store.dispatch(new ActionPreferencesUpdateUserSettings({ notDisplayConnectivityAfterAddDevice: true }));
this.dialogRef.close(null);
} else {
this.dialogRef.close(null);
}
}
createMarkDownCommand(command: string): string {
return '```bash\n' +
command +
'{:copy-code}\n' +
'```';
}
private loadCommands() {
this.deviceService.getDevicePublishTelemetryCommands(this.data.deviceId.id).subscribe(
commands => {
this.commands = commands;
const commandsProtocols = Object.keys(commands);
this.transportTypes.forEach(transport => {
const findCommand = commandsProtocols.find(item => item.toUpperCase().startsWith(transport));
if (findCommand) {
this.allowTransportType.add(transport);
}
});
this.selectTransportType = this.allowTransportType.values().next().value;
this.loadedCommand = true;
}
);
}
private subscribeToLatestTelemetry() {
this.store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe(
take(1)
).subscribe(persistToTelemetry => {
this.telemetrySubscriber = TelemetrySubscriber.createEntityAttributesSubscription(
this.telemetryWsService, this.data.deviceId, LatestTelemetry.LATEST_TELEMETRY, this.zone);
if (!persistToTelemetry) {
const subscriptionCommand = new AttributesSubscriptionCmd();
subscriptionCommand.entityType = this.data.deviceId.entityType as EntityType;
subscriptionCommand.entityId = this.data.deviceId.id;
subscriptionCommand.scope = AttributeScope.SERVER_SCOPE;
subscriptionCommand.keys = 'active';
this.telemetrySubscriber.subscriptionCommands.push(subscriptionCommand);
}
this.telemetrySubscriber.subscribe();
this.telemetrySubscriber.attributeData$().subscribe(
(data) => {
this.latestTelemetry = data.reduce<Array<AttributeData>>((accumulator, item) => {
if (item.key === 'active') {
this.status = item.value;
} else if (item.lastUpdateTs > this.currentTime) {
accumulator.push(item);
}
return accumulator;
}, []);
}
);
});
}
}

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

@ -46,6 +46,12 @@
[fxShow]="!isEdit">
{{ ((deviceScope === 'customer_user' || deviceScope === 'edge_customer_user') ? 'device.view-credentials' : 'device.manage-credentials') | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'checkConnectivity')"
[fxShow]="!isEdit">
{{ 'device.connectivity.check-connectivity' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'unassignFromEdge')"

4
ui-ngx/src/app/modules/home/pages/device/device.module.ts

@ -35,6 +35,7 @@ import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-
import { SnmpDeviceTransportConfigurationComponent } from './data/snmp-device-transport-configuration.component';
import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module';
import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module';
import { DeviceCheckConnectivityDialogComponent } from './device-check-connectivity-dialog.component';
@NgModule({
declarations: [
@ -50,7 +51,8 @@ import { DeviceProfileCommonModule } from '@home/components/profile/device/commo
DeviceComponent,
DeviceTabsComponent,
DeviceTableHeaderComponent,
DeviceCredentialsDialogComponent
DeviceCredentialsDialogComponent,
DeviceCheckConnectivityDialogComponent
],
imports: [
CommonModule,

35
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -40,7 +40,7 @@ import {
import { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of, Subject } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { selectAuthUser } from '@core/auth/auth.selectors';
import { selectAuthUser, selectUserSettingsProperty } from '@core/auth/auth.selectors';
import { map, mergeMap, take, tap } from 'rxjs/operators';
import { AppState } from '@core/core.state';
import { DeviceService } from '@app/core/http/device.service';
@ -79,6 +79,11 @@ import { EdgeId } from '@shared/models/id/edge-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
import {
DeviceCheckConnectivityDialogComponent,
DeviceCheckConnectivityDialogData
} from '@home/pages/device/device-check-connectivity-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
interface DevicePageQueryParams extends PageQueryParam {
deviceProfileId?: string;
@ -456,13 +461,20 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
deviceWizard($event: Event) {
this.dialog.open<DeviceWizardDialogComponent, AddEntityDialogData<BaseData<HasId>>,
boolean>(DeviceWizardDialogComponent, {
Device>(DeviceWizardDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(res) => {
if (res) {
this.config.updateData();
this.store.pipe(select(selectUserSettingsProperty( 'notDisplayConnectivityAfterAddDevice'))).pipe(
take(1)
).subscribe((settings: boolean) => {
if(!settings) {
this.checkConnectivity(null, res.id, true);
}
});
}
}
);
@ -631,6 +643,9 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
case 'manageCredentials':
this.manageCredentials(action.event, action.entity);
return true;
case 'checkConnectivity':
this.checkConnectivity(action.event, action.entity.id);
return true;
}
return false;
}
@ -705,4 +720,20 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
);
}
checkConnectivity($event: Event, deviceId: EntityId, showDontShowAgain = false) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<DeviceCheckConnectivityDialogComponent, DeviceCheckConnectivityDialogData>
(DeviceCheckConnectivityDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
deviceId,
showDontShowAgain
}
})
.afterClosed()
.subscribe(() => {});
}
}

2
ui-ngx/src/app/shared/components/markdown.component.scss

@ -88,7 +88,7 @@
}
}
a {
a:not(.ignore-style-a) {
font-weight: 500;
color: #2a7dec;
text-decoration: none;

16
ui-ngx/src/app/shared/models/device.models.ts

@ -52,6 +52,12 @@ export enum DeviceTransportType {
SNMP = 'SNMP'
}
export enum BasicTransportType {
HTTP = 'HTTP'
}
export type TransportType = BasicTransportType | DeviceTransportType;
export type NetworkTransportType = BasicTransportType | Exclude<DeviceTransportType, DeviceTransportType.DEFAULT>;
export enum TransportPayloadType {
JSON = 'JSON',
PROTOBUF = 'PROTOBUF'
@ -99,13 +105,14 @@ export const deviceProfileTypeConfigurationInfoMap = new Map<DeviceProfileType,
]
);
export const deviceTransportTypeTranslationMap = new Map<DeviceTransportType, string>(
export const deviceTransportTypeTranslationMap = new Map<TransportType, string>(
[
[DeviceTransportType.DEFAULT, 'device-profile.transport-type-default'],
[DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt'],
[DeviceTransportType.COAP, 'device-profile.transport-type-coap'],
[DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m'],
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp']
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp'],
[BasicTransportType.HTTP, 'device-profile.transport-type-http']
]
);
@ -119,13 +126,14 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
]
);
export const deviceTransportTypeHintMap = new Map<DeviceTransportType, string>(
export const deviceTransportTypeHintMap = new Map<TransportType, string>(
[
[DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'],
[DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'],
[DeviceTransportType.COAP, 'device-profile.transport-type-coap-hint'],
[DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint'],
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp-hint']
[DeviceTransportType.SNMP, 'device-profile.transport-type-snmp-hint'],
[BasicTransportType.HTTP, '']
]
);

1
ui-ngx/src/app/shared/models/user-settings.models.ts

@ -16,6 +16,7 @@
export interface UserSettings {
openedMenuSections?: string[];
notDisplayConnectivityAfterAddDevice?: boolean;
}
export const initialUserSettings: UserSettings = {

40
ui-ngx/src/assets/help/en_US/device/install_coap_client.md

@ -0,0 +1,40 @@
#### CoAP installation instructions
---
<br />
Install coap client tool on your **Linux/macOS**:
```bash
git clone https://github.com/obgm/libcoap --recursive
{:copy-code}
```
<br />
```bash
cd libcoap
{:copy-code}
```
<br />
```bash
./autogen.sh
{:copy-code}
```
<br />
```bash
./configure --with-openssl --disable-doxygen --disable-manpages --disable-shared
{:copy-code}
```
<br />
```bash
make
{:copy-code}
```
<br />
```bash
sudo make install
{:copy-code}
```

34
ui-ngx/src/assets/help/en_US/device/install_curl.md

@ -0,0 +1,34 @@
#### cURL installation instructions
---
<section style="margin: 18px 0">
<div style="margin: 0 32px">
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false" appearance="fill">
<tb-toggle-option value="ubuntu">Ubuntu</tb-toggle-option>
<tb-toggle-option value="macos">MacOS</tb-toggle-option>
<tb-toggle-option value="windows">Windows</tb-toggle-option>
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="publishCommandSteps.value">
<ng-template [ngSwitchCase]="'ubuntu'">
<p style="margin-top: 18px; padding: 0 32px">Install cURL tool:</p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
sudo apt-get install curl
{:copy-code}
```
"></tb-markdown>
</ng-template>
<ng-template [ngSwitchCase]="'macos'">
<p style="margin-top: 18px; padding: 0 32px">Install cURL tool:</p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
brew install curl
{:copy-code}
```
"></tb-markdown>
</ng-template>
<ng-template [ngSwitchCase]="'windows'">
<div style="margin-top: 18px; padding: 0 32px; width: 404px;">Starting Windows 10 b17063, cURL is available by default.</div>
</ng-template>
</ng-container>
</section>

38
ui-ngx/src/assets/help/en_US/device/install_mqtt_client.md

@ -0,0 +1,38 @@
#### MQTT client tool installation instructions
---
<section style="margin: 18px 0">
<div style="margin: 0 32px">
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false" appearance="fill">
<tb-toggle-option value="ubuntu">Ubuntu</tb-toggle-option>
<tb-toggle-option value="macos">MacOS</tb-toggle-option>
<tb-toggle-option value="windows">Windows</tb-toggle-option>
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="publishCommandSteps.value">
<ng-template [ngSwitchCase]="'ubuntu'">
<p style="margin-top: 18px; padding: 0 32px">Install mqtt client tool:</p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
sudo apt-get install curl mosquitto-clients
{:copy-code}
```
"></tb-markdown>
</ng-template>
<ng-template [ngSwitchCase]="'macos'">
<p style="margin-top: 18px; padding: 0 32px">Install mqtt client tool:</p>
<tb-markdown usePlainMarkdown containerClass="tb-getting-started-code" data="
```bash
brew install mosquitto-clients
{:copy-code}
```
"></tb-markdown>
</ng-template>
<ng-template [ngSwitchCase]="'windows'">
<p style="margin-top: 18px; padding: 0 32px">Install mqtt client tool:</p>
<a mat-stroked-button color="primary" class="ignore-style-a"
style="margin: 8px 32px"
href="https://thingsboard.io/docs/getting-started-guides/helloworld/?connectdevice=mqtt-windows#step-2-connect-device" target="_blank">
<mat-icon>description</mat-icon>How to install MQTT Box</a>
</ng-template>
</ng-container>
</section>

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

@ -69,7 +69,9 @@
"skip": "Skip",
"send": "Send",
"reset": "Reset",
"show-more": "Show more"
"show-more": "Show more",
"dont-show-again": "Do not show again",
"see-documentation": "See documentation"
},
"aggregation": {
"aggregation": "Aggregation",
@ -691,6 +693,7 @@
"attribute": {
"attributes": "Attributes",
"latest-telemetry": "Latest telemetry",
"no-latest-telemetry": "No latest telemetry",
"attributes-scope": "Entity attributes scope",
"scope-latest-telemetry": "Latest telemetry",
"scope-client": "Client attributes",
@ -1379,7 +1382,26 @@
"customer-to-assign-device": "Customer to assign the device"
},
"unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 device} other {# devices} }?",
"unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge."
"unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge.",
"time": "Time",
"connectivity": {
"check-connectivity": "Check connectivity",
"loading-check-connectivity-command": "Loading check connectivity commands...",
"use-following-instructions": "Use the following instructions for sending telemetry on behalf of the device using shell",
"install-curl": "Install cURL tool.",
"install-mqtt-client": "Install mgtt client tool.",
"install-coap-cli": "Install coap-cli tool.",
"http-command": "HTTP (Linux, macOS or Windows)",
"https-command": "HTTPS (Linux, macOS or Windows)",
"mqtt-command": "MQTT (Linux, macOS)",
"mqtts-command": "MQTT over SSL (Linux, macOS)",
"mqtts-x509-command": "Use the following documentation to connect the device via MQTT with authorization X509",
"coap-command": "CoAP (Linux, macOS)",
"coaps-command": "CoAP over DTLS (Linux, macOS)",
"coaps-x509-command": "Use the following documentation to connect the device via CoAP over DTLS with authorization X509",
"snmp-command": "Use the following documentation to connect the device through the SNMP.",
"lwm2m-command": "Use the following documentation to connect the device through the LWM2M."
}
},
"asset-profile": {
"asset-profile": "Asset profile",
@ -1461,6 +1483,7 @@
"transport-type-lwm2m-hint": "LWM2M transport type",
"transport-type-snmp": "SNMP",
"transport-type-snmp-hint": "Specify SNMP transport configuration",
"transport-type-http": "HTTP",
"description": "Description",
"default": "Default",
"profile-configuration": "Profile configuration",

49
ui-ngx/src/form.scss

@ -284,24 +284,6 @@
gap: 12px;
padding-bottom: 12px;
.tb-form-table-header {
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 12px;
padding-left: 12px;
&-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
}
}
.tb-form-table-body {
display: flex;
flex-direction: column;
@ -313,12 +295,31 @@
}
}
.tb-form-table-row {
height: 38px;
.tb-form-table-header, .tb-form-table-row {
display: flex;
flex-direction: row;
gap: 12px;
padding-left: 12px;
place-content: center flex-start;
align-items: center;
&-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
}
}
.tb-form-table-header {
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
&-cell {
color: rgba(0, 0, 0, 0.54);
}
}
.tb-form-table-row {
height: 38px;
&.tb-draggable {
gap: 0;
@ -326,6 +327,10 @@
background: #fff;
}
&-cell {
color: rgba(0, 0, 0, 0.87);
}
&-cell-buttons {
display: flex;
flex-direction: row;
@ -365,10 +370,6 @@
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;

21
ui-ngx/src/typings/utils.d.ts

@ -0,0 +1,21 @@
///
/// 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.
///
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]> extends infer U extends string ? U : never}`
: `${Key}`
}[keyof ObjectType & (string | number)];
Loading…
Cancel
Save