Browse Source

UI: Redesign device wizard and device credentias

pull/8882/head
Vladyslav_Prykhodko 3 years ago
parent
commit
b781a05764
  1. 7
      ui-ngx/src/app/core/http/device.service.ts
  2. 7
      ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html
  3. 13
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  4. 23
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.scss
  5. 48
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  6. 5
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  7. 6
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.scss
  8. 17
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
  9. 137
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  10. 51
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
  11. 319
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  12. 90
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html
  13. 41
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.scss
  14. 2
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
  15. 5
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  16. 7
      ui-ngx/src/app/shared/components/toggle-header.component.html
  17. 27
      ui-ngx/src/app/shared/components/toggle-header.component.scss
  18. 4
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  19. 1
      ui-ngx/src/app/shared/components/toggle-select.component.html
  20. 2
      ui-ngx/src/app/shared/components/toggle-select.component.ts
  21. 2
      ui-ngx/src/app/shared/models/device.models.ts
  22. 9
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  23. 9
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  24. 9
      ui-ngx/src/assets/locale/locale.constant-da_DK.json
  25. 9
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  26. 9
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  27. 9
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  28. 9
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  29. 9
      ui-ngx/src/assets/locale/locale.constant-sl_SI.json
  30. 9
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  31. 9
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  32. 9
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json

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

@ -87,6 +87,13 @@ export class DeviceService {
return this.http.post<Device>('/api/device', device, defaultHttpOptionsFromConfig(config));
}
public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable<Device> {
return this.http.post<Device>('/api/device-with-credentials', {
device,
credentials
}, defaultHttpOptionsFromConfig(config));
}
public deleteDevice(deviceId: string, config?: RequestConfig) {
return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config));
}

7
ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html

@ -26,13 +26,14 @@
matTooltip="{{ 'device.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('clientId')"
*ngIf="!deviceCredentialsMqttFormGroup.get('clientId').value; else copyClientId">
*ngIf="!deviceCredentialsMqttFormGroup.get('clientId').value && !disabled; else copyClientId">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyClientId>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="deviceCredentialsMqttFormGroup.get('clientId').value"
[copyText]="deviceCredentialsMqttFormGroup.get('clientId').value"
tooltipText="{{ 'device.copy-client-id' | translate }}"
tooltipPosition="above"
@ -53,7 +54,7 @@
matTooltip="{{ 'device.generate-user-name' | translate }}"
matTooltipPosition="above"
(click)="generate('userName')"
*ngIf="!deviceCredentialsMqttFormGroup.get('userName').value; else copyUserName">
*ngIf="!deviceCredentialsMqttFormGroup.get('userName').value && !disabled; else copyUserName">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyUserName>
@ -85,7 +86,7 @@
matTooltip="{{ 'device.generate-password' | translate }}"
matTooltipPosition="above"
(click)="generate('password')"
*ngIf="!deviceCredentialsMqttFormGroup.get('password').value; else copyPassword">
*ngIf="!deviceCredentialsMqttFormGroup.get('password').value && !disabled; else copyPassword">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyPassword>

13
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -16,7 +16,7 @@
-->
<section [formGroup]="deviceCredentialsFormGroup">
<mat-form-field class="mat-block" [fxShow]="credentialsTypes?.length > 1">
<mat-form-field class="mat-block" [fxShow.xs]="credentialsTypes?.length > 1" fxHide.gt-xs>
<mat-label translate>device.credentials-type</mat-label>
<mat-select formControlName="credentialsType">
<mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
@ -24,6 +24,14 @@
</mat-option>
</mat-select>
</mat-form-field>
<section [fxShow.gt-xs]="credentialsTypes?.length > 1" fxHide.xs style="margin-bottom: 16px">
<div class="tb-type-title" translate>device.credentials-type</div>
<tb-toggle-select formControlName="credentialsType" appearance="fill">
<tb-toggle-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
{{ credentialTypeNamesMap.get(credentialsType) }}
</tb-toggle-option>
</tb-toggle-select>
</section>
<div [ngSwitch]="deviceCredentialsFormGroup.get('credentialsType').value">
<ng-template [ngSwitchCase]="deviceCredentialsType.ACCESS_TOKEN">
<mat-form-field class="mat-block">
@ -36,13 +44,14 @@
matTooltip="{{ 'device.generate-access-token' | translate }}"
matTooltipPosition="above"
(click)="generate('credentialsId')"
*ngIf="!deviceCredentialsFormGroup.get('credentialsId').value; else copyAccessToken">
*ngIf="!deviceCredentialsFormGroup.get('credentialsId').value && !disabled; else copyAccessToken">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyAccessToken>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="deviceCredentialsFormGroup.get('credentialsId').value"
[copyText]="deviceCredentialsFormGroup.get('credentialsId').value"
tooltipText="{{ 'device.copy-access-token' | translate }}"
tooltipPosition="above"

23
ui-ngx/src/app/modules/home/components/device/device-credentials.component.scss

@ -0,0 +1,23 @@
/**
* 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.
*/
:host{
.tb-type-title {
font-size: 12px;
line-height: 16px;
letter-spacing: 0.25px;
margin-bottom: 8px;
}
}

48
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -17,9 +17,9 @@
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
ControlValueAccessor,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
@ -35,6 +35,7 @@ import {
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { generateSecret, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-device-credentials',
@ -50,7 +51,7 @@ import { generateSecret, isDefinedAndNotNull } from '@core/utils';
useExisting: forwardRef(() => DeviceCredentialsComponent),
multi: true,
}],
styleUrls: []
styleUrls: ['./device-credentials.component.scss']
})
export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
@ -73,9 +74,13 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
}
}
@Input()
@coerceBoolean()
initAccessToken = false;
private destroy$ = new Subject<void>();
deviceCredentialsFormGroup: UntypedFormGroup;
deviceCredentialsFormGroup: FormGroup;
deviceCredentialsType = DeviceCredentialsType;
@ -83,9 +88,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
credentialTypeNamesMap = credentialTypeNames;
private propagateChange = (v: any) => {};
private propagateChange = null;
private propagateChangePending = false;
constructor(public fb: UntypedFormBuilder) {
constructor(public fb: FormBuilder) {
this.deviceCredentialsFormGroup = this.fb.group({
credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
credentialsId: [null],
@ -98,8 +104,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
});
this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.credentialsTypeChanged();
).subscribe((value) => {
this.credentialsTypeChanged(value);
});
}
@ -107,6 +113,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
if (this.disabled) {
this.deviceCredentialsFormGroup.disable({emitEvent: false});
}
if (this.initAccessToken && !this.deviceCredentialsFormGroup.get('credentialsId').value &&
this.deviceCredentialsFormGroup.get('credentialsType').value === DeviceCredentialsType.ACCESS_TOKEN) {
this.deviceCredentialsFormGroup.get('credentialsId').patchValue(generateSecret(20));
}
}
ngOnDestroy() {
@ -128,11 +138,21 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
updateView() {
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
this.propagateChange(deviceCredentialsValue);
if (this.propagateChange) {
this.propagateChange(deviceCredentialsValue);
} else {
this.propagateChangePending = true;
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
setTimeout(() => {
this.updateView();
}, 0);
}
}
registerOnTouched(fn: any): void {}
@ -144,11 +164,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
} else {
this.deviceCredentialsFormGroup.enable({emitEvent: false});
this.updateValidators();
this.deviceCredentialsFormGroup.updateValueAndValidity();
}
}
public validate(c: UntypedFormControl) {
public validate(c: FormControl) {
return this.deviceCredentialsFormGroup.valid ? null : {
deviceCredentials: {
valid: false,
@ -156,12 +175,15 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
};
}
credentialsTypeChanged(): void {
credentialsTypeChanged(type: DeviceCredentialsType): void {
this.deviceCredentialsFormGroup.patchValue({
credentialsId: null,
credentialsValue: null
});
this.updateValidators();
if (type === DeviceCredentialsType.ACCESS_TOKEN && this.initAccessToken) {
this.deviceCredentialsFormGroup.get('credentialsId').patchValue(generateSecret(20));
}
}
updateValidators(): void {

5
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

@ -43,6 +43,11 @@
(click)="editDeviceProfile($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button mat-button color="primary" matSuffix
(click)="createDeviceProfile($event, '')"
*ngIf="!selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && addNewProfile && showCreateNewButton">
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span>
</button>
<mat-autocomplete
class="tb-autocomplete"
(closed)="onPanelClosed()"

6
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.scss

@ -19,3 +19,9 @@
color: inherit;
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

17
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts

@ -49,6 +49,7 @@ import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './a
import { emptyPageData } from '@shared/models/page/page-data';
import { getEntityDetailsPageURL } from '@core/utils';
import { SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-device-profile-autocomplete',
@ -70,6 +71,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
@coerceBoolean()
selectDefaultProfile = false;
@Input()
@ -79,25 +81,26 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
displayAllOnEmpty = false;
@Input()
@coerceBoolean()
editProfileEnabled = true;
@Input()
@coerceBoolean()
addNewProfile = true;
@Input()
@coerceBoolean()
showCreateNewButton = false;
@Input()
showDetailsPageLink = false;
@Input()
transportType: DeviceTransportType = null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@coerceBoolean()
required = false;
@Input()
disabled: boolean;

137
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html

@ -27,9 +27,12 @@
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
<mat-horizontal-stepper #addDeviceWizardStepper
[linear]="true"
[labelPosition]="(stepperLabelPosition | async)"
[orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
@ -54,58 +57,27 @@
{{ 'device.label-max-length' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="16px">
<mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
<mat-radio-button [value]="0" color="primary">
<span translate>device.wizard.existing-device-profile</span>
</mat-radio-button>
<mat-radio-button [value]="1" color="primary">
<span translate>device.wizard.new-device-profile</span>
</mat-radio-button>
</mat-radio-group>
<div fxLayout="column">
<tb-device-profile-autocomplete
[required]="!createProfile"
formControlName="deviceProfileId"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[editProfileEnabled]="false"
(deviceProfileChanged)="deviceProfileChanged($event)">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}">
<mat-label translate>device-profile.new-device-profile-name</mat-label>
<input matInput formControlName="newDeviceProfileTitle"
[required]="createProfile">
<mat-error *ngIf="deviceWizardFormGroup.get('newDeviceProfileTitle').hasError('required')">
{{ 'device-profile.new-device-profile-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-rule-chain-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-queue-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-autocomplete>
</div>
</div>
<div fxLayout="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<mat-checkbox formControlName="gateway">
<tb-device-profile-autocomplete
required
addNewProfile
showCreateNewButton
selectDefaultProfile
formControlName="deviceProfileId"
(deviceProfileChanged)="deviceProfileChanged($event)">
</tb-device-profile-autocomplete>
<tb-entity-autocomplete
formControlName="customerId"
labelText="device.wizard.customer-to-assign-device"
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
<div class="toggle-group">
<mat-slide-toggle formControlName="gateway" style="min-width: 165px">
{{ 'device.is-gateway' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="deviceWizardFormGroup.get('gateway').value"
</mat-slide-toggle>
<mat-slide-toggle *ngIf="deviceWizardFormGroup.get('gateway').value"
formControlName="overwriteActivityTime">
{{ 'device.overwrite-activity-time' | translate }}
</mat-checkbox>
</mat-slide-toggle>
</div>
<mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label>
@ -114,73 +86,20 @@
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template><mat-form-field class="mat-block" style="padding-bottom: 14px;">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="transportConfigFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>
<tb-device-profile-provision-configuration
formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</form>
</mat-step>
<mat-step [stepControl]="credentialsFormGroup" [optional]="true">
<mat-step [stepControl]="credentialsFormGroup" [optional]="credentialsOptionalStep">
<ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
<form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;">
<mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credentials' | translate }}</mat-checkbox>
<tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
[deviceTransportType]="deviceTransportType"
initAccessToken
formControlName="credential">
</tb-device-credentials>
</form>
</mat-step>
<mat-step [stepControl]="customerFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'customer.customer' | translate }}</ng-template>
<form [formGroup]="customerFormGroup" style="padding-bottom: 16px;">
<tb-entity-autocomplete
formControlName="customerId"
labelText="device.wizard.customer-to-assign-device"
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions style="padding: 0">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center">
<div mat-dialog-actions class="tb-dialog-actions">
<div class="dialog-actions-row">
<button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
@ -192,7 +111,7 @@
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<mat-divider style="width: 100%"></mat-divider>
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end center">
<div class="dialog-actions-row">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>

51
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss

@ -18,49 +18,62 @@
:host {
height: 100%;
display: grid;
grid-template-rows: min-content 4px auto min-content;
.dialog-actions-row {
padding: 8px;
.toggle-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
}
@media #{$mat-sm} {
min-width: 470px;
}
@media #{$mat-gt-sm} {
min-width: 650px;
}
}
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) {
@media #{$mat-lt-sm} {
.mat-mdc-dialog-content {
max-height: 75vh;
:host-context(.mat-mdc-dialog-container) {
.tb-dialog-actions {
padding: 0;
grid-row: 4;
.dialog-actions-row {
padding: 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 1;
}
}
.invisible{
visibility: hidden;
.mat-mdc-dialog-content {
grid-row: 3;
padding: 0;
}
}
:host ::ng-deep {
.mat-mdc-dialog-content {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 !important;
.mat-stepper-horizontal {
display: flex;
height: 100%;
overflow: hidden;
.mat-horizontal-stepper-wrapper {
flex: 1 1 100%;
width: 100%;
}
.mat-horizontal-content-container {
height: 680px;
height: 500px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
scrollbar-gutter: stable;
@media #{$mat-gt-sm} {
min-width: 500px;
}
}
}
}

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

@ -14,201 +14,83 @@
/// limitations under the License.
///
import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Component, ViewChild } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
createDeviceProfileConfiguration,
createDeviceProfileTransportConfiguration,
DeviceProfile,
DeviceProfileInfo,
DeviceProfileType,
DeviceProvisionConfiguration,
DeviceProvisionType,
DeviceTransportType,
deviceTransportTypeHintMap,
deviceTransportTypeTranslationMap
} from '@shared/models/device.models';
import { MatStepper } from '@angular/material/stepper';
import { AddEntityDialogData } from '@home/models/entity/entity-component.models';
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 { DeviceProfileService } from '@core/http/device-profile.service';
import { EntityId } from '@shared/models/id/entity-id';
import { Observable, of, Subscription, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { DeviceService } from '@core/http/device.service';
import { ErrorStateMatcher } from '@angular/material/core';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { ServiceType } from '@shared/models/queue.models';
import { deepTrim } from '@core/utils';
import { CustomerId } from '@shared/models/id/customer-id';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'tb-device-wizard',
templateUrl: './device-wizard-dialog.component.html',
providers: [],
styleUrls: ['./device-wizard-dialog.component.scss']
})
export class DeviceWizardDialogComponent extends
DialogComponent<DeviceWizardDialogComponent, boolean> implements OnDestroy, ErrorStateMatcher {
export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDialogComponent, boolean> {
@ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper;
selectedIndex = 0;
showNext = true;
createProfile = false;
entityType = EntityType;
deviceTransportTypes = Object.values(DeviceTransportType);
deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
stepperOrientation: Observable<StepperOrientation>;
deviceTransportTypeHints = deviceTransportTypeHintMap;
stepperLabelPosition: Observable<'bottom' | 'end'>;
deviceWizardFormGroup: UntypedFormGroup;
transportConfigFormGroup: UntypedFormGroup;
alarmRulesFormGroup: UntypedFormGroup;
selectedIndex = 0;
provisionConfigFormGroup: UntypedFormGroup;
credentialsOptionalStep = true;
credentialsFormGroup: UntypedFormGroup;
showNext = true;
customerFormGroup: UntypedFormGroup;
entityType = EntityType;
labelPosition: MatStepper['labelPosition'] = 'end';
deviceWizardFormGroup: FormGroup;
serviceType = ServiceType.TB_RULE_ENGINE;
credentialsFormGroup: FormGroup;
private subscriptions: Subscription[] = [];
private currentDeviceProfileTransportType = DeviceTransportType.DEFAULT;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<EntityId>>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>,
private deviceProfileService: DeviceProfileService,
private deviceService: DeviceService,
private breakpointObserver: BreakpointObserver,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
super(store, router, dialogRef);
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
this.stepperLabelPosition = this.breakpointObserver.observe(MediaBreakpoints['gt-sm'])
.pipe(map(({matches}) => matches ? 'end' : 'bottom'));
this.deviceWizardFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(255)]],
label: ['', Validators.maxLength(255)],
gateway: [false],
overwriteActivityTime: [false],
addProfileType: [0],
customerId: [null],
deviceProfileId: [null, Validators.required],
newDeviceProfileTitle: [{value: null, disabled: true}],
defaultRuleChainId: [{value: null, disabled: true}],
defaultQueueName: [{value: null, disabled: true}],
description: ['']
}
);
this.subscriptions.push(this.deviceWizardFormGroup.get('addProfileType').valueChanges.subscribe(
(addProfileType: number) => {
if (addProfileType === 0) {
this.deviceWizardFormGroup.get('deviceProfileId').setValidators([Validators.required]);
this.deviceWizardFormGroup.get('deviceProfileId').enable();
this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators(null);
this.deviceWizardFormGroup.get('newDeviceProfileTitle').disable();
this.deviceWizardFormGroup.get('defaultRuleChainId').disable();
this.deviceWizardFormGroup.get('defaultQueueName').disable();
this.deviceWizardFormGroup.updateValueAndValidity();
this.createProfile = false;
} else {
this.deviceWizardFormGroup.get('deviceProfileId').setValidators(null);
this.deviceWizardFormGroup.get('deviceProfileId').disable();
this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]);
this.deviceWizardFormGroup.get('newDeviceProfileTitle').enable();
this.deviceWizardFormGroup.get('defaultRuleChainId').enable();
this.deviceWizardFormGroup.get('defaultQueueName').enable();
this.deviceWizardFormGroup.updateValueAndValidity();
this.createProfile = true;
}
}
));
this.transportConfigFormGroup = this.fb.group(
{
transportType: [DeviceTransportType.DEFAULT, Validators.required],
transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required]
}
);
this.subscriptions.push(this.transportConfigFormGroup.get('transportType').valueChanges.subscribe((transportType) => {
this.deviceProfileTransportTypeChanged(transportType);
}));
this.alarmRulesFormGroup = this.fb.group({
alarms: [null]
}
);
this.provisionConfigFormGroup = this.fb.group(
{
provisionConfiguration: [{
type: DeviceProvisionType.DISABLED
} as DeviceProvisionConfiguration, [Validators.required]]
}
);
this.credentialsFormGroup = this.fb.group({
setCredential: [false],
credential: [{value: null, disabled: true}]
}
);
this.subscriptions.push(this.credentialsFormGroup.get('setCredential').valueChanges.subscribe((value) => {
if (value) {
this.credentialsFormGroup.get('credential').enable();
} else {
this.credentialsFormGroup.get('credential').disable();
}
}));
this.customerFormGroup = this.fb.group({
customerId: [null]
credential: []
}
);
this.labelPosition = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']) ? 'end' : 'bottom';
this.subscriptions.push(this.breakpointObserver
.observe(MediaBreakpoints['gt-sm'])
.subscribe((state: BreakpointState) => {
if (state.matches) {
this.labelPosition = 'end';
} else {
this.labelPosition = 'bottom';
}
}
));
}
ngOnDestroy() {
super.ngOnDestroy();
this.subscriptions.forEach(s => s.unsubscribe());
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid);
return originalErrorState || customErrorState;
}
cancel(): void {
@ -224,24 +106,11 @@ export class DeviceWizardDialogComponent extends
}
getFormLabel(index: number): string {
if (index > 0) {
if (!this.createProfile) {
index += 3;
}
}
switch (index) {
case 0:
return 'device.wizard.device-details';
case 1:
return 'device-profile.transport-configuration';
case 2:
return 'device-profile.alarm-rules';
case 3:
return 'device-profile.device-provisioning';
case 4:
return 'device.credentials';
case 5:
return 'customer.customer';
}
}
@ -249,88 +118,30 @@ export class DeviceWizardDialogComponent extends
return this.addDeviceWizardStepper?._steps?.length - 1;
}
private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void {
this.transportConfigFormGroup.patchValue(
{transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)});
const setCredentialBox = this.credentialsFormGroup.get('setCredential');
if (deviceTransportType === DeviceTransportType.LWM2M) {
setCredentialBox.patchValue(true);
setCredentialBox.disable();
} else {
setCredentialBox.patchValue(false);
setCredentialBox.enable();
}
}
add(): void {
if (this.allValid()) {
this.createDeviceProfile().pipe(
mergeMap(profileId => this.createDevice(profileId)),
mergeMap(device => this.saveCredentials(device))
).subscribe(
(created) => {
this.dialogRef.close(created);
}
this.createDevice().subscribe(
() => this.dialogRef.close(true)
);
}
}
get deviceTransportType(): DeviceTransportType {
if (this.deviceWizardFormGroup.get('addProfileType').value) {
return this.transportConfigFormGroup.get('transportType').value;
} else {
return this.currentDeviceProfileTransportType;
}
return this.currentDeviceProfileTransportType;
}
deviceProfileChanged(deviceProfile: DeviceProfileInfo) {
if (deviceProfile) {
this.currentDeviceProfileTransportType = deviceProfile.transportType;
this.credentialsOptionalStep = this.currentDeviceProfileTransportType !== DeviceTransportType.LWM2M;
}
}
private createDeviceProfile(): Observable<EntityId> {
if (this.deviceWizardFormGroup.get('addProfileType').value) {
const deviceProvisionConfiguration: DeviceProvisionConfiguration = this.provisionConfigFormGroup.get('provisionConfiguration').value;
const provisionDeviceKey = deviceProvisionConfiguration.provisionDeviceKey;
delete deviceProvisionConfiguration.provisionDeviceKey;
const deviceProfile: DeviceProfile = {
name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value,
type: DeviceProfileType.DEFAULT,
defaultQueueName: this.deviceWizardFormGroup.get('defaultQueueName').value,
transportType: this.transportConfigFormGroup.get('transportType').value,
provisionType: deviceProvisionConfiguration.type,
provisionDeviceKey,
profileData: {
configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
alarms: this.alarmRulesFormGroup.get('alarms').value,
provisionConfiguration: deviceProvisionConfiguration
}
};
if (this.deviceWizardFormGroup.get('defaultRuleChainId').value) {
deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceWizardFormGroup.get('defaultRuleChainId').value);
}
return this.deviceProfileService.saveDeviceProfile(deepTrim(deviceProfile)).pipe(
tap((profile) => {
this.currentDeviceProfileTransportType = profile.transportType;
this.deviceWizardFormGroup.patchValue({
deviceProfileId: profile.id,
addProfileType: 0
});
}),
map(profile => profile.id)
);
} else {
return of(this.deviceWizardFormGroup.get('deviceProfileId').value);
}
}
private createDevice(profileId): Observable<BaseData<HasId>> {
const device = {
private createDevice(): Observable<BaseData<HasId>> {
const device: Device = {
name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value,
deviceProfileId: profileId,
deviceProfileId: this.deviceWizardFormGroup.get('deviceProfileId').value,
additionalInfo: {
gateway: this.deviceWizardFormGroup.get('gateway').value,
overwriteActivityTime: this.deviceWizardFormGroup.get('overwriteActivityTime').value,
@ -338,13 +149,22 @@ export class DeviceWizardDialogComponent extends
},
customerId: null
};
if (this.customerFormGroup.get('customerId').value) {
device.customerId = {
entityType: EntityType.CUSTOMER,
id: this.customerFormGroup.get('customerId').value
};
if (this.deviceWizardFormGroup.get('customerId').value) {
device.customerId = new CustomerId(this.deviceWizardFormGroup.get('customerId').value);
}
if (this.addDeviceWizardStepper.steps.last.completed || this.addDeviceWizardStepper.selectedIndex > 0) {
return this.deviceService.saveDeviceWithCredentials(deepTrim(device), deepTrim(this.credentialsFormGroup.value.credential)).pipe(
catchError((e: HttpErrorResponse) => {
if (e.error.message.include('Device credentials')) {
this.addDeviceWizardStepper.selectedIndex = 1;
} else {
this.addDeviceWizardStepper.selectedIndex = 0;
}
return throwError(() => e);
})
);
}
return this.data.entitiesTableConfig.saveEntity(deepTrim(device)).pipe(
return this.deviceService.saveDevice(deepTrim(device)).pipe(
catchError(e => {
this.addDeviceWizardStepper.selectedIndex = 0;
return throwError(e);
@ -352,31 +172,8 @@ export class DeviceWizardDialogComponent extends
);
}
private saveCredentials(device: BaseData<HasId>): Observable<boolean> {
if (this.credentialsFormGroup.get('setCredential').value) {
return this.deviceService.getDeviceCredentials(device.id.id).pipe(
mergeMap(
(deviceCredentials) => {
const deviceCredentialsValue = {...deviceCredentials, ...this.credentialsFormGroup.value.credential};
return this.deviceService.saveDeviceCredentials(deviceCredentialsValue).pipe(
catchError(e => {
this.addDeviceWizardStepper.selectedIndex = 1;
return this.deviceService.deleteDevice(device.id.id).pipe(
mergeMap(() => {
return throwError(e);
}
));
})
);
}
),
map(() => true));
}
return of(true);
}
allValid(): boolean {
if (this.addDeviceWizardStepper.steps.find((item, index) => {
return !this.addDeviceWizardStepper.steps.find((item, index) => {
if (item.stepControl.invalid) {
item.interacted = true;
this.addDeviceWizardStepper.selectedIndex = index;
@ -384,19 +181,11 @@ export class DeviceWizardDialogComponent extends
} else {
return false;
}
} )) {
return false;
} else {
return true;
}
});
}
changeStep($event: StepperSelectionEvent): void {
this.selectedIndex = $event.selectedIndex;
if (this.selectedIndex === this.maxStepperIndex) {
this.showNext = false;
} else {
this.showNext = true;
}
this.showNext = this.selectedIndex !== this.maxStepperIndex;
}
}

90
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html

@ -15,49 +15,47 @@
limitations under the License.
-->
<form [formGroup]="deviceCredentialsFormGroup" (ngSubmit)="save()" style="min-width: 350px;">
<mat-toolbar color="primary">
<h2>{{ 'device.device-credentials' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="(isLoading$ | async) && !loadingCredentials">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async) || loadingCredentials"></div>
<div mat-dialog-content>
<section *ngIf="!loadingCredentials; else loadCredentials">
<fieldset [disabled]="(isLoading$ | async) || isReadOnly">
<tb-device-credentials
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</fieldset>
</section>
<ng-template #loadCredentials>
<div fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" diameter="65" strokeWidth="4" style="margin-bottom: 18px"></mat-spinner>
<span class="mat-subtitle-1" style="margin-bottom: 0">
{{ 'device.loading-device-credentials' | translate }}
</span>
</div>
</ng-template>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (isReadOnly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button *ngIf="!isReadOnly" mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || deviceCredentialsFormGroup.invalid
|| !deviceCredentialsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>
<mat-toolbar color="primary">
<h2>{{ 'device.device-credentials' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="(isLoading$ | async)">
</mat-progress-bar>
<div mat-dialog-content>
<section *ngIf="!loadingCredentials; else loadCredentials">
<form [formGroup]="deviceCredentialsFormGroup">
<tb-device-credentials
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</form>
</section>
<ng-template #loadCredentials>
<div fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" diameter="65" strokeWidth="4" style="margin-bottom: 18px"></mat-spinner>
<span class="mat-subtitle-1" style="margin-bottom: 0">
{{ 'device.loading-device-credentials' | translate }}
</span>
</div>
</ng-template>
</div>
<div mat-dialog-actions class="tb-dialog-actions">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (isReadOnly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button *ngIf="!isReadOnly" mat-raised-button color="primary"
type="button"
(click)="save()"
[disabled]="(isLoading$ | async) || deviceCredentialsFormGroup.invalid
|| !deviceCredentialsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>

41
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.scss

@ -0,0 +1,41 @@
/**
* 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%;
display: grid;
grid-template-rows: min-content 4px auto min-content;
@media #{$mat-gt-xs} {
min-width: 420px;
}
}
:host-context(.mat-mdc-dialog-container) {
.tb-dialog-actions {
grid-row: 4;
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 1;
}
.mat-mdc-dialog-content {
grid-row: 3;
padding: 24px 24px 4px;
}
}

2
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts

@ -37,7 +37,7 @@ export interface DeviceCredentialsDialogData {
selector: 'tb-device-credentials-dialog',
templateUrl: './device-credentials-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}],
styleUrls: []
styleUrls: ['./device-credentials-dialog.component.scss']
})
export class DeviceCredentialsDialogComponent extends
DialogComponent<DeviceCredentialsDialogComponent, DeviceCredentials> implements OnInit, ErrorStateMatcher {

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

@ -458,10 +458,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.dialog.open<DeviceWizardDialogComponent, AddEntityDialogData<BaseData<HasId>>,
boolean>(DeviceWizardDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entitiesTableConfig: this.config
}
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(res) => {
if (res) {

7
ui-ngx/src/app/shared/components/toggle-header.component.html

@ -18,13 +18,14 @@
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': appearance === 'fill-invert',
'tb-ignore-md-lg': ignoreMdLgSize}" [name]="name" [(ngModel)]="value"
'tb-ignore-md-lg': ignoreMdLgSize,
'tb-disabled': disabled }" [name]="name" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value">{{ option.name }}</mat-button-toggle>
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
<ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)">
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)" [disabled]="disabled">
<mat-option *ngFor="let option of options" [value]="option.value"> {{ option.name }}</mat-option>
</mat-select>
</mat-form-field>

27
ui-ngx/src/app/shared/components/toggle-header.component.scss

@ -80,6 +80,33 @@
}
}
}
&.tb-disabled {
pointer-events: none;
background: rgba(0, 0, 0, 0.03);
.mat-button-toggle.mat-button-toggle-appearance-standard {
color: rgba(0, 0, 0, 0.28);
&.mat-button-toggle-checked {
.mat-button-toggle-button {
background: transparent;
color: rgba(0, 0, 0, 0.38);
border-color: rgba(0, 0, 0, 0.38);
}
}
}
&.tb-fill {
.mat-button-toggle.mat-button-toggle-appearance-standard {
&.mat-button-toggle-checked {
.mat-button-toggle-button {
background: rgba(0, 0, 0, 0.12);
color: rgba(0, 0, 0, 0.38);
border: transparent;
}
}
}
}
}
}
@media #{$mat-md-lg} {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header:not(.tb-ignore-md-lg) {

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

@ -131,6 +131,10 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC
@Input()
appearance: ToggleHeaderAppearance = 'stroked';
@Input()
@coerceBoolean()
disabled = false;
isMdLg: boolean;
private observeBreakpointSubscription: Subscription;

1
ui-ngx/src/app/shared/components/toggle-select.component.html

@ -18,6 +18,7 @@
<tb-toggle-header
ignoreMdLgSize="true"
useSelectOnMdLg="false"
[disabled]="disabled"
[appearance]="appearance"
[options]="options"
[value]="modelValue"

2
ui-ngx/src/app/shared/components/toggle-select.component.ts

@ -19,6 +19,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { _ToggleBase, ToggleHeaderAppearance } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-toggle-select',
@ -35,6 +36,7 @@ import { _ToggleBase, ToggleHeaderAppearance } from '@shared/components/toggle-h
export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor {
@Input()
@coerceBoolean()
disabled: boolean;
@Input()

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

@ -705,7 +705,7 @@ export interface Device extends BaseData<DeviceId>, ExportableEntity<DeviceId> {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;
type: string;
type?: string;
label: string;
firmwareId?: OtaPackageId;
softwareId?: OtaPackageId;

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

@ -1376,13 +1376,8 @@
"device-configuration": "Configuració del dispositiu",
"transport-configuration": "Configuració del transport",
"wizard": {
"device-wizard": "Assistent de dispositiu",
"device-details": "Detalls del dispositiu",
"new-device-profile": "Crear un nou perfil de dispositiu",
"existing-device-profile": "Seleccionar un perfil existent",
"specific-configuration": "Configuració específica",
"customer-to-assign-device": "Client al que assignar el dispositiu",
"add-credentials": "Afegir credencial"
"customer-to-assign-device": "Client al que assignar el dispositiu"
},
"unassign-devices-from-edge-title": "Està segur de que desitja desassignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?",
"unassign-devices-from-edge-text": "Després de la confirmació, tots els dispositius seleccionats quedaran sense assignar i la vora no podrà accedir a ells."
@ -1404,8 +1399,6 @@
"delete": "Esborrar perfil de dispositiu",
"copyId": "Copiar ID de perfil",
"name-max-length": "El nom ha de ser inferior a 256",
"new-device-profile-name": "Nom del perfil",
"new-device-profile-name-required": "Cal nom de perfil.",
"name": "Nom",
"name-required": "Cal nom.",
"type": "Tipus de perfil",

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

@ -1018,13 +1018,8 @@
"device-configuration": "Konfigurace zařízení",
"transport-configuration": "Konfigurace přenosu",
"wizard": {
"device-wizard": "Průvodce zařízením",
"device-details": "Detail zařízení",
"new-device-profile": "Vytvořit nový profil zařízení",
"existing-device-profile": "Vybrat existující profil zařízení",
"specific-configuration": "Specifická konfigurace",
"customer-to-assign-device": "Přiřadit zařízení zákazníkovi",
"add-credentials": "Přidat přístupový údaj"
"customer-to-assign-device": "Přiřadit zařízení zákazníkovi"
},
"unassign-devices-from-edge-title": "Jste se jisti, že chcete odebrat { count, plural, =1 {1 zařízení} other {# zařízení} }?",
"unassign-devices-from-edge-text": "Po potvrzení budou všechna vybraná zařízení odebrána a nebudou pro edge dostupná."
@ -1045,8 +1040,6 @@
"set-default": "Učinit profil zařízení defaultním",
"delete": "Smazat profil zařízení",
"copyId": "Kopírovat Id profilu zařízení",
"new-device-profile-name": "Název profilu zařízení",
"new-device-profile-name-required": "Název profilu zařízení je povinný.",
"name": "Název",
"name-required": "Název je povinný.",
"type": "Typ profilu",

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

@ -1095,13 +1095,8 @@
"device-configuration": "Enhedskonfiguration",
"transport-configuration": "Transportkonfiguration",
"wizard": {
"device-wizard": "Enhedsguide",
"device-details": "Enhedsoplysninger",
"new-device-profile": "Opret ny enhedsprofil",
"existing-device-profile": "Vælg eksisterende enhedsprofil",
"specific-configuration": "Specifik konfiguration",
"customer-to-assign-device": "Kunden skal tildele enheden",
"add-credential": "Tilføj brugeroplysninger"
"customer-to-assign-device": "Kunden skal tildele enheden"
}
},
"device-profile": {
@ -1120,8 +1115,6 @@
"set-default": "Gør enhedsprofil standard",
"delete": "Slet enhedsprofil",
"copyId": "Kopiér enhedsprofil-id",
"new-device-profile-name": "Enhedsprofilnavn",
"new-device-profile-name-required": "Enhedsprofilnavn er påkrævet.",
"name": "Navn",
"name-required": "Navn er påkrævet.",
"type": "Profiltype",

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

@ -1373,13 +1373,8 @@
"device-configuration": "Device configuration",
"transport-configuration": "Transport configuration",
"wizard": {
"device-wizard": "Device Wizard",
"device-details": "Device details",
"new-device-profile": "Create new device profile",
"existing-device-profile": "Select existing device profile",
"specific-configuration": "Specific configuration",
"customer-to-assign-device": "Customer to assign the device",
"add-credentials": "Add credentials"
"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."
@ -1446,8 +1441,6 @@
"delete": "Delete device profile",
"copyId": "Copy device profile Id",
"name-max-length": "Name should be less than 256",
"new-device-profile-name": "Device profile name",
"new-device-profile-name-required": "Device profile name is required.",
"name": "Name",
"name-required": "Name is required.",
"type": "Profile type",

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

@ -1325,13 +1325,8 @@
"device-configuration": "Configuración del dispositivo",
"transport-configuration": "Configuración del transporte",
"wizard": {
"device-wizard": "Asistente de dispositivo",
"device-details": "Detalles del dispositivo",
"new-device-profile": "Crear un nuevo perfil de dispositivo",
"existing-device-profile": "Seleccionar un perfil existente",
"specific-configuration": "Configuración específica",
"customer-to-assign-device": "Cliente al que asignar el dispositivo",
"add-credentials": "Añadir credencial"
"customer-to-assign-device": "Cliente al que asignar el dispositivo"
},
"unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?",
"unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el Edge no podrá acceder a ellos."
@ -1398,8 +1393,6 @@
"delete": "Borrar perfil de dispositivo",
"copyId": "Copiar ID de perfil",
"name-max-length": "El nombre debe ser menor de 256",
"new-device-profile-name": "Nombre de perfil",
"new-device-profile-name-required": "Se requiere nombre de perfil.",
"name": "Nombre",
"name-required": "Se requiere nombre.",
"type": "Tipo de perfil",

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

@ -1053,13 +1053,8 @@
"device-configuration": "Configuration du dipositif",
"transport-configuration": "Configuration du transport",
"wizard": {
"device-wizard": "Wizard du dispositif",
"device-details": "Détails du dispositif",
"new-device-profile": "Créer un nouveau profil de dispositif",
"existing-device-profile": "Choisissez un profile de dispositif existant",
"specific-configuration": "Configuration spécifique",
"customer-to-assign-device": "Client auquel assigner le dispositif",
"add-credentials": "Ajouter identifiants"
"customer-to-assign-device": "Client auquel assigner le dispositif"
}
},
"device-profile": {
@ -1079,8 +1074,6 @@
"delete": "Supprimer le profil de dispositif",
"copyId": "Copier l'Identifiant du profil de dispositif",
"name-max-length": "La longueur du nom devrait être moins de 256",
"new-device-profile-name": "Nom du profil de dispositif",
"new-device-profile-name-required": "Nom du profil de dispositif est requis.",
"name": "Nom",
"name-required": "Nom est requis.",
"type": "Type de profile",

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

@ -913,13 +913,8 @@
"device-configuration": "장치 설정",
"transport-configuration": "전송 설정",
"wizard": {
"device-wizard": "장치 마법사",
"device-details": "장치 상세 정보",
"new-device-profile": "새로운 장치 프로파일 생성",
"existing-device-profile": "기존 장치 프로파일 선택",
"specific-configuration": "특수 설정",
"customer-to-assign-device": "장치에 할당할 커스터머",
"add-credentials": "크리덴셜 추가"
"customer-to-assign-device": "장치에 할당할 커스터머"
}
},
"device-profile": {
@ -938,8 +933,6 @@
"set-default": "Make device profile default",
"delete": "Delete device profile",
"copyId": "Copy device profile Id",
"new-device-profile-name": "장치 프로파일 이름",
"new-device-profile-name-required": "Device profile name is required.",
"name": "이름",
"name-required": "이름을 입력하세요.",
"type": "프로파일 유형",

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

@ -913,13 +913,8 @@
"device-configuration": "Device configuration",
"transport-configuration": "Transport configuration",
"wizard": {
"device-wizard": "Device Wizard",
"device-details": "Device details",
"new-device-profile": "Create new device profile",
"existing-device-profile": "Select existing device profile",
"specific-configuration": "Specific configuration",
"customer-to-assign-device": "Customer to assign the device",
"add-credentials": "Add credentials"
"customer-to-assign-device": "Customer to assign the device"
}
},
"device-profile": {
@ -938,8 +933,6 @@
"set-default": "Make device profile default",
"delete": "Delete device profile",
"copyId": "Copy device profile Id",
"new-device-profile-name": "Device profile name",
"new-device-profile-name-required": "Device profile name is required.",
"name": "Name",
"name-required": "Name is required.",
"type": "Profile type",

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

@ -1021,13 +1021,8 @@
"device-configuration": "Cihaz yapılandırması",
"transport-configuration": "Aktarım yapılandırması",
"wizard": {
"device-wizard": "Cihaz Sihirbazı",
"device-details": "Cihaz ayrıntıları",
"new-device-profile": "Yeni cihaz profili oluştur",
"existing-device-profile": "Mevcut cihaz profilini seçin",
"specific-configuration": "Özel yapılandırma",
"customer-to-assign-device": "Cihazı atamak için kullanıcı grubu",
"add-credentials": "Kimlik bilgileri ekle"
"customer-to-assign-device": "Cihazı atamak için kullanıcı grubu"
},
"unassign-devices-from-edge-title": "{ count, plural, =1 {1 cihazın} other {# cihazın} } atamasını kaldırmak istediğinizden emin misiniz?",
"unassign-devices-from-edge-text": "Onaydan sonra, seçilen tüm cihazların ataması kaldırılacak ve uç tarafından erişilemeyecek."
@ -1048,8 +1043,6 @@
"set-default": "Cihaz profilini varsayılan yap",
"delete": "Cihaz profilini sil",
"copyId": "Cihaz profili kimliğini kopyala",
"new-device-profile-name": "Cihaz profili adı",
"new-device-profile-name-required": "Cihaz profili adı gerekli.",
"name": "İsim",
"name-required": "İsim gerekli.",
"type": "Profil türü",

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

@ -1221,13 +1221,8 @@
"device-configuration": "设备配置",
"transport-configuration": "传输配置",
"wizard": {
"device-wizard": "设备向导",
"device-details": "设备详细信息",
"new-device-profile": "新建设备配置",
"existing-device-profile": "选择已有设备配置",
"specific-configuration": "指定配置",
"customer-to-assign-device": "客户分配设备",
"add-credentials": "添加凭据"
"customer-to-assign-device": "客户分配设备"
},
"unassign-devices-from-edge-title": "确定要取消分配 { count, plural, =1 {1 个设备} other {# 个设备} } 吗?",
"unassign-devices-from-edge-text": "确认后,设备将被取消分配,边缘将无法访问。"
@ -1292,8 +1287,6 @@
"delete": "删除设备配置",
"copyId": "复制设备配置 ID",
"name-max-length": "名称长度必须少于256个字符",
"new-device-profile-name": "设备配置名称",
"new-device-profile-name-required": "设备配置名称必填。",
"name": "名称",
"name-required": "名称是必需的。",
"type": "配置类型",

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

@ -1134,13 +1134,8 @@
"device-configuration": "設備配置",
"transport-configuration": "傳輸配置",
"wizard": {
"device-wizard": "設備嚮導",
"device-details": "設備詳情",
"new-device-profile": "建立設備協議",
"existing-device-profile": "選擇現有的設備協議",
"specific-configuration": "具體配置",
"customer-to-assign-device": "客戶指定設備",
"add-credentials": "新增驗證資訊"
"customer-to-assign-device": "客戶指定設備"
},
"unassign-devices-from-edge-title": "您確定要解除邊緣設備 { count, plural, =1 {1 device} other {# devices} }的指定嗎?",
"unassign-devices-from-edge-text": "確認後邊緣指定設備將解除指定及其所有相關資料將無法恢復。"
@ -1205,8 +1200,6 @@
"delete": "刪除設備協議",
"copyId": "複製設備協議Id",
"name-max-length": "名稱應小於256",
"new-device-profile-name": "設備協議名稱",
"new-device-profile-name-required": "需要設備協議名稱。",
"name": "名稱",
"name-required": "需要名稱",
"type": "協議類型",

Loading…
Cancel
Save