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)); 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) { public deleteDevice(deviceId: string, config?: RequestConfig) {
return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config)); 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 }}" matTooltip="{{ 'device.generate-client-id' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="generate('clientId')" (click)="generate('clientId')"
*ngIf="!deviceCredentialsMqttFormGroup.get('clientId').value; else copyClientId"> *ngIf="!deviceCredentialsMqttFormGroup.get('clientId').value && !disabled; else copyClientId">
<mat-icon>autorenew</mat-icon> <mat-icon>autorenew</mat-icon>
</button> </button>
<ng-template #copyClientId> <ng-template #copyClientId>
<tb-copy-button <tb-copy-button
matSuffix matSuffix
miniButton="false" miniButton="false"
*ngIf="deviceCredentialsMqttFormGroup.get('clientId').value"
[copyText]="deviceCredentialsMqttFormGroup.get('clientId').value" [copyText]="deviceCredentialsMqttFormGroup.get('clientId').value"
tooltipText="{{ 'device.copy-client-id' | translate }}" tooltipText="{{ 'device.copy-client-id' | translate }}"
tooltipPosition="above" tooltipPosition="above"
@ -53,7 +54,7 @@
matTooltip="{{ 'device.generate-user-name' | translate }}" matTooltip="{{ 'device.generate-user-name' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="generate('userName')" (click)="generate('userName')"
*ngIf="!deviceCredentialsMqttFormGroup.get('userName').value; else copyUserName"> *ngIf="!deviceCredentialsMqttFormGroup.get('userName').value && !disabled; else copyUserName">
<mat-icon>autorenew</mat-icon> <mat-icon>autorenew</mat-icon>
</button> </button>
<ng-template #copyUserName> <ng-template #copyUserName>
@ -85,7 +86,7 @@
matTooltip="{{ 'device.generate-password' | translate }}" matTooltip="{{ 'device.generate-password' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="generate('password')" (click)="generate('password')"
*ngIf="!deviceCredentialsMqttFormGroup.get('password').value; else copyPassword"> *ngIf="!deviceCredentialsMqttFormGroup.get('password').value && !disabled; else copyPassword">
<mat-icon>autorenew</mat-icon> <mat-icon>autorenew</mat-icon>
</button> </button>
<ng-template #copyPassword> <ng-template #copyPassword>

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

@ -16,7 +16,7 @@
--> -->
<section [formGroup]="deviceCredentialsFormGroup"> <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-label translate>device.credentials-type</mat-label>
<mat-select formControlName="credentialsType"> <mat-select formControlName="credentialsType">
<mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType"> <mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
@ -24,6 +24,14 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </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"> <div [ngSwitch]="deviceCredentialsFormGroup.get('credentialsType').value">
<ng-template [ngSwitchCase]="deviceCredentialsType.ACCESS_TOKEN"> <ng-template [ngSwitchCase]="deviceCredentialsType.ACCESS_TOKEN">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
@ -36,13 +44,14 @@
matTooltip="{{ 'device.generate-access-token' | translate }}" matTooltip="{{ 'device.generate-access-token' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="generate('credentialsId')" (click)="generate('credentialsId')"
*ngIf="!deviceCredentialsFormGroup.get('credentialsId').value; else copyAccessToken"> *ngIf="!deviceCredentialsFormGroup.get('credentialsId').value && !disabled; else copyAccessToken">
<mat-icon>autorenew</mat-icon> <mat-icon>autorenew</mat-icon>
</button> </button>
<ng-template #copyAccessToken> <ng-template #copyAccessToken>
<tb-copy-button <tb-copy-button
matSuffix matSuffix
miniButton="false" miniButton="false"
*ngIf="deviceCredentialsFormGroup.get('credentialsId').value"
[copyText]="deviceCredentialsFormGroup.get('credentialsId').value" [copyText]="deviceCredentialsFormGroup.get('credentialsId').value"
tooltipText="{{ 'device.copy-access-token' | translate }}" tooltipText="{{ 'device.copy-access-token' | translate }}"
tooltipPosition="above" 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 { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
UntypedFormBuilder, FormBuilder,
UntypedFormControl, FormControl,
UntypedFormGroup, FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
Validator, Validator,
@ -35,6 +35,7 @@ import {
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { generateSecret, isDefinedAndNotNull } from '@core/utils'; import { generateSecret, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-device-credentials', selector: 'tb-device-credentials',
@ -50,7 +51,7 @@ import { generateSecret, isDefinedAndNotNull } from '@core/utils';
useExisting: forwardRef(() => DeviceCredentialsComponent), useExisting: forwardRef(() => DeviceCredentialsComponent),
multi: true, multi: true,
}], }],
styleUrls: [] styleUrls: ['./device-credentials.component.scss']
}) })
export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { 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>(); private destroy$ = new Subject<void>();
deviceCredentialsFormGroup: UntypedFormGroup; deviceCredentialsFormGroup: FormGroup;
deviceCredentialsType = DeviceCredentialsType; deviceCredentialsType = DeviceCredentialsType;
@ -83,9 +88,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
credentialTypeNamesMap = credentialTypeNames; 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({ this.deviceCredentialsFormGroup = this.fb.group({
credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
credentialsId: [null], credentialsId: [null],
@ -98,8 +104,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
}); });
this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.pipe( this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe(() => { ).subscribe((value) => {
this.credentialsTypeChanged(); this.credentialsTypeChanged(value);
}); });
} }
@ -107,6 +113,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
if (this.disabled) { if (this.disabled) {
this.deviceCredentialsFormGroup.disable({emitEvent: false}); 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() { ngOnDestroy() {
@ -128,11 +138,21 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
updateView() { updateView() {
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value; const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
this.propagateChange(deviceCredentialsValue); if (this.propagateChange) {
this.propagateChange(deviceCredentialsValue);
} else {
this.propagateChangePending = true;
}
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
setTimeout(() => {
this.updateView();
}, 0);
}
} }
registerOnTouched(fn: any): void {} registerOnTouched(fn: any): void {}
@ -144,11 +164,10 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
} else { } else {
this.deviceCredentialsFormGroup.enable({emitEvent: false}); this.deviceCredentialsFormGroup.enable({emitEvent: false});
this.updateValidators(); this.updateValidators();
this.deviceCredentialsFormGroup.updateValueAndValidity();
} }
} }
public validate(c: UntypedFormControl) { public validate(c: FormControl) {
return this.deviceCredentialsFormGroup.valid ? null : { return this.deviceCredentialsFormGroup.valid ? null : {
deviceCredentials: { deviceCredentials: {
valid: false, valid: false,
@ -156,12 +175,15 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
}; };
} }
credentialsTypeChanged(): void { credentialsTypeChanged(type: DeviceCredentialsType): void {
this.deviceCredentialsFormGroup.patchValue({ this.deviceCredentialsFormGroup.patchValue({
credentialsId: null, credentialsId: null,
credentialsValue: null credentialsValue: null
}); });
this.updateValidators(); this.updateValidators();
if (type === DeviceCredentialsType.ACCESS_TOKEN && this.initAccessToken) {
this.deviceCredentialsFormGroup.get('credentialsId').patchValue(generateSecret(20));
}
} }
updateValidators(): void { updateValidators(): void {

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

@ -43,6 +43,11 @@
(click)="editDeviceProfile($event)"> (click)="editDeviceProfile($event)">
<mat-icon class="material-icons">edit</mat-icon> <mat-icon class="material-icons">edit</mat-icon>
</button> </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 <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
(closed)="onPanelClosed()" (closed)="onPanelClosed()"

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

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

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

@ -27,9 +27,12 @@
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <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"> <ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon> <mat-icon>check</mat-icon>
</ng-template> </ng-template>
@ -54,58 +57,27 @@
{{ 'device.label-max-length' | translate }} {{ 'device.label-max-length' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayoutGap="16px"> <tb-device-profile-autocomplete
<mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around"> required
<mat-radio-button [value]="0" color="primary"> addNewProfile
<span translate>device.wizard.existing-device-profile</span> showCreateNewButton
</mat-radio-button> selectDefaultProfile
<mat-radio-button [value]="1" color="primary"> formControlName="deviceProfileId"
<span translate>device.wizard.new-device-profile</span> (deviceProfileChanged)="deviceProfileChanged($event)">
</mat-radio-button> </tb-device-profile-autocomplete>
</mat-radio-group> <tb-entity-autocomplete
<div fxLayout="column"> formControlName="customerId"
<tb-device-profile-autocomplete labelText="device.wizard.customer-to-assign-device"
[required]="!createProfile" [entityType]="entityType.CUSTOMER">
formControlName="deviceProfileId" </tb-entity-autocomplete>
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}" <div class="toggle-group">
[addNewProfile]="false" <mat-slide-toggle formControlName="gateway" style="min-width: 165px">
[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">
{{ 'device.is-gateway' | translate }} {{ 'device.is-gateway' | translate }}
</mat-checkbox> </mat-slide-toggle>
<mat-checkbox *ngIf="deviceWizardFormGroup.get('gateway').value" <mat-slide-toggle *ngIf="deviceWizardFormGroup.get('gateway').value"
formControlName="overwriteActivityTime"> formControlName="overwriteActivityTime">
{{ 'device.overwrite-activity-time' | translate }} {{ 'device.overwrite-activity-time' | translate }}
</mat-checkbox> </mat-slide-toggle>
</div> </div>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label> <mat-label translate>device.description</mat-label>
@ -114,73 +86,20 @@
</fieldset> </fieldset>
</form> </form>
</mat-step> </mat-step>
<mat-step [stepControl]="transportConfigFormGroup" [optional]="true" *ngIf="createProfile"> <mat-step [stepControl]="credentialsFormGroup" [optional]="credentialsOptionalStep">
<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">
<ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template> <ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
<form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;"> <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 <tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
[deviceTransportType]="deviceTransportType" [deviceTransportType]="deviceTransportType"
initAccessToken
formControlName="credential"> formControlName="credential">
</tb-device-credentials> </tb-device-credentials>
</form> </form>
</mat-step> </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> </mat-horizontal-stepper>
</div> </div>
<div mat-dialog-actions style="padding: 0"> <div mat-dialog-actions class="tb-dialog-actions">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center"> <div class="dialog-actions-row">
<button mat-stroked-button *ngIf="selectedIndex > 0" <button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button> (click)="previousStep()">{{ 'action.back' | translate }}</button>
@ -192,7 +111,7 @@
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button> (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div> </div>
<mat-divider style="width: 100%"></mat-divider> <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 <button mat-button
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button> (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 { :host {
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-rows: min-content 4px auto min-content;
.dialog-actions-row { .toggle-group {
padding: 8px; 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) { :host-context(.mat-mdc-dialog-container) {
@media #{$mat-lt-sm} { .tb-dialog-actions {
.mat-mdc-dialog-content { padding: 0;
max-height: 75vh; grid-row: 4;
.dialog-actions-row {
padding: 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 1;
} }
} }
.invisible{ .mat-mdc-dialog-content {
visibility: hidden; grid-row: 3;
padding: 0;
} }
} }
:host ::ng-deep { :host ::ng-deep {
.mat-mdc-dialog-content { .mat-mdc-dialog-content {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 !important;
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex; display: flex;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
.mat-horizontal-stepper-wrapper { .mat-horizontal-stepper-wrapper {
flex: 1 1 100%; width: 100%;
} }
.mat-horizontal-content-container { .mat-horizontal-content-container {
height: 680px; height: 500px;
max-height: 100%; max-height: 100%;
width: 100%;;
overflow-y: auto; overflow-y: auto;
scrollbar-gutter: stable; 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. /// limitations under the License.
/// ///
import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; import { Component, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
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 { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { import { Device, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
createDeviceProfileConfiguration, import { MatStepper, StepperOrientation } from '@angular/material/stepper';
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 { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DeviceProfileService } from '@core/http/device-profile.service'; import { Observable, throwError } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id'; import { catchError, map } from 'rxjs/operators';
import { Observable, of, Subscription, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { DeviceService } from '@core/http/device.service'; import { DeviceService } from '@core/http/device.service';
import { ErrorStateMatcher } from '@angular/material/core';
import { StepperSelectionEvent } from '@angular/cdk/stepper'; 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 { 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 { deepTrim } from '@core/utils';
import { CustomerId } from '@shared/models/id/customer-id';
import { HttpErrorResponse } from '@angular/common/http';
@Component({ @Component({
selector: 'tb-device-wizard', selector: 'tb-device-wizard',
templateUrl: './device-wizard-dialog.component.html', templateUrl: './device-wizard-dialog.component.html',
providers: [],
styleUrls: ['./device-wizard-dialog.component.scss'] styleUrls: ['./device-wizard-dialog.component.scss']
}) })
export class DeviceWizardDialogComponent extends export class DeviceWizardDialogComponent extends DialogComponent<DeviceWizardDialogComponent, boolean> {
DialogComponent<DeviceWizardDialogComponent, boolean> implements OnDestroy, ErrorStateMatcher {
@ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper; @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper;
selectedIndex = 0; stepperOrientation: Observable<StepperOrientation>;
showNext = true;
createProfile = false;
entityType = EntityType;
deviceTransportTypes = Object.values(DeviceTransportType);
deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
deviceTransportTypeHints = deviceTransportTypeHintMap; stepperLabelPosition: Observable<'bottom' | 'end'>;
deviceWizardFormGroup: UntypedFormGroup; selectedIndex = 0;
transportConfigFormGroup: UntypedFormGroup;
alarmRulesFormGroup: UntypedFormGroup;
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; private currentDeviceProfileTransportType = DeviceTransportType.DEFAULT;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<EntityId>>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>, public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>,
private deviceProfileService: DeviceProfileService,
private deviceService: DeviceService, private deviceService: DeviceService,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); 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({ this.deviceWizardFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(255)]], name: ['', [Validators.required, Validators.maxLength(255)]],
label: ['', Validators.maxLength(255)], label: ['', Validators.maxLength(255)],
gateway: [false], gateway: [false],
overwriteActivityTime: [false], overwriteActivityTime: [false],
addProfileType: [0], customerId: [null],
deviceProfileId: [null, Validators.required], deviceProfileId: [null, Validators.required],
newDeviceProfileTitle: [{value: null, disabled: true}],
defaultRuleChainId: [{value: null, disabled: true}],
defaultQueueName: [{value: null, disabled: true}],
description: [''] 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({ this.credentialsFormGroup = this.fb.group({
setCredential: [false], credential: []
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]
} }
); );
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 { cancel(): void {
@ -224,24 +106,11 @@ export class DeviceWizardDialogComponent extends
} }
getFormLabel(index: number): string { getFormLabel(index: number): string {
if (index > 0) {
if (!this.createProfile) {
index += 3;
}
}
switch (index) { switch (index) {
case 0: case 0:
return 'device.wizard.device-details'; return 'device.wizard.device-details';
case 1: 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'; return 'device.credentials';
case 5:
return 'customer.customer';
} }
} }
@ -249,88 +118,30 @@ export class DeviceWizardDialogComponent extends
return this.addDeviceWizardStepper?._steps?.length - 1; 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 { add(): void {
if (this.allValid()) { if (this.allValid()) {
this.createDeviceProfile().pipe( this.createDevice().subscribe(
mergeMap(profileId => this.createDevice(profileId)), () => this.dialogRef.close(true)
mergeMap(device => this.saveCredentials(device))
).subscribe(
(created) => {
this.dialogRef.close(created);
}
); );
} }
} }
get deviceTransportType(): DeviceTransportType { get deviceTransportType(): DeviceTransportType {
if (this.deviceWizardFormGroup.get('addProfileType').value) { return this.currentDeviceProfileTransportType;
return this.transportConfigFormGroup.get('transportType').value;
} else {
return this.currentDeviceProfileTransportType;
}
} }
deviceProfileChanged(deviceProfile: DeviceProfileInfo) { deviceProfileChanged(deviceProfile: DeviceProfileInfo) {
if (deviceProfile) { if (deviceProfile) {
this.currentDeviceProfileTransportType = deviceProfile.transportType; this.currentDeviceProfileTransportType = deviceProfile.transportType;
this.credentialsOptionalStep = this.currentDeviceProfileTransportType !== DeviceTransportType.LWM2M;
} }
} }
private createDeviceProfile(): Observable<EntityId> { private createDevice(): Observable<BaseData<HasId>> {
if (this.deviceWizardFormGroup.get('addProfileType').value) { const device: Device = {
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 = {
name: this.deviceWizardFormGroup.get('name').value, name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value, label: this.deviceWizardFormGroup.get('label').value,
deviceProfileId: profileId, deviceProfileId: this.deviceWizardFormGroup.get('deviceProfileId').value,
additionalInfo: { additionalInfo: {
gateway: this.deviceWizardFormGroup.get('gateway').value, gateway: this.deviceWizardFormGroup.get('gateway').value,
overwriteActivityTime: this.deviceWizardFormGroup.get('overwriteActivityTime').value, overwriteActivityTime: this.deviceWizardFormGroup.get('overwriteActivityTime').value,
@ -338,13 +149,22 @@ export class DeviceWizardDialogComponent extends
}, },
customerId: null customerId: null
}; };
if (this.customerFormGroup.get('customerId').value) { if (this.deviceWizardFormGroup.get('customerId').value) {
device.customerId = { device.customerId = new CustomerId(this.deviceWizardFormGroup.get('customerId').value);
entityType: EntityType.CUSTOMER, }
id: this.customerFormGroup.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 => { catchError(e => {
this.addDeviceWizardStepper.selectedIndex = 0; this.addDeviceWizardStepper.selectedIndex = 0;
return throwError(e); 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 { allValid(): boolean {
if (this.addDeviceWizardStepper.steps.find((item, index) => { return !this.addDeviceWizardStepper.steps.find((item, index) => {
if (item.stepControl.invalid) { if (item.stepControl.invalid) {
item.interacted = true; item.interacted = true;
this.addDeviceWizardStepper.selectedIndex = index; this.addDeviceWizardStepper.selectedIndex = index;
@ -384,19 +181,11 @@ export class DeviceWizardDialogComponent extends
} else { } else {
return false; return false;
} }
} )) { });
return false;
} else {
return true;
}
} }
changeStep($event: StepperSelectionEvent): void { changeStep($event: StepperSelectionEvent): void {
this.selectedIndex = $event.selectedIndex; this.selectedIndex = $event.selectedIndex;
if (this.selectedIndex === this.maxStepperIndex) { this.showNext = this.selectedIndex !== this.maxStepperIndex;
this.showNext = false;
} else {
this.showNext = true;
}
} }
} }

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

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

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', selector: 'tb-device-credentials-dialog',
templateUrl: './device-credentials-dialog.component.html', templateUrl: './device-credentials-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}], providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}],
styleUrls: [] styleUrls: ['./device-credentials-dialog.component.scss']
}) })
export class DeviceCredentialsDialogComponent extends export class DeviceCredentialsDialogComponent extends
DialogComponent<DeviceCredentialsDialogComponent, DeviceCredentials> implements OnInit, ErrorStateMatcher { 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>>, this.dialog.open<DeviceWizardDialogComponent, AddEntityDialogData<BaseData<HasId>>,
boolean>(DeviceWizardDialogComponent, { boolean>(DeviceWizardDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
data: {
entitiesTableConfig: this.config
}
}).afterClosed().subscribe( }).afterClosed().subscribe(
(res) => { (res) => {
if (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" <mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'), [ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': 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)"> (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> </mat-button-toggle-group>
<ng-template #select> <ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic"> <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-option *ngFor="let option of options" [value]="option.value"> {{ option.name }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </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} { @media #{$mat-md-lg} {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header:not(.tb-ignore-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() @Input()
appearance: ToggleHeaderAppearance = 'stroked'; appearance: ToggleHeaderAppearance = 'stroked';
@Input()
@coerceBoolean()
disabled = false;
isMdLg: boolean; isMdLg: boolean;
private observeBreakpointSubscription: Subscription; private observeBreakpointSubscription: Subscription;

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

@ -18,6 +18,7 @@
<tb-toggle-header <tb-toggle-header
ignoreMdLgSize="true" ignoreMdLgSize="true"
useSelectOnMdLg="false" useSelectOnMdLg="false"
[disabled]="disabled"
[appearance]="appearance" [appearance]="appearance"
[options]="options" [options]="options"
[value]="modelValue" [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 { AppState } from '@core/core.state';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { _ToggleBase, ToggleHeaderAppearance } from '@shared/components/toggle-header.component'; import { _ToggleBase, ToggleHeaderAppearance } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-toggle-select', selector: 'tb-toggle-select',
@ -35,6 +36,7 @@ import { _ToggleBase, ToggleHeaderAppearance } from '@shared/components/toggle-h
export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor { export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor {
@Input() @Input()
@coerceBoolean()
disabled: boolean; disabled: boolean;
@Input() @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; tenantId?: TenantId;
customerId?: CustomerId; customerId?: CustomerId;
name: string; name: string;
type: string; type?: string;
label: string; label: string;
firmwareId?: OtaPackageId; firmwareId?: OtaPackageId;
softwareId?: OtaPackageId; softwareId?: OtaPackageId;

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

@ -1376,13 +1376,8 @@
"device-configuration": "Configuració del dispositiu", "device-configuration": "Configuració del dispositiu",
"transport-configuration": "Configuració del transport", "transport-configuration": "Configuració del transport",
"wizard": { "wizard": {
"device-wizard": "Assistent de dispositiu",
"device-details": "Detalls del dispositiu", "device-details": "Detalls del dispositiu",
"new-device-profile": "Crear un nou perfil de dispositiu", "customer-to-assign-device": "Client al que assignar el 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"
}, },
"unassign-devices-from-edge-title": "Està segur de que desitja desassignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?", "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." "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", "delete": "Esborrar perfil de dispositiu",
"copyId": "Copiar ID de perfil", "copyId": "Copiar ID de perfil",
"name-max-length": "El nom ha de ser inferior a 256", "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": "Nom",
"name-required": "Cal nom.", "name-required": "Cal nom.",
"type": "Tipus de perfil", "type": "Tipus de perfil",

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

@ -1018,13 +1018,8 @@
"device-configuration": "Konfigurace zařízení", "device-configuration": "Konfigurace zařízení",
"transport-configuration": "Konfigurace přenosu", "transport-configuration": "Konfigurace přenosu",
"wizard": { "wizard": {
"device-wizard": "Průvodce zařízením",
"device-details": "Detail zařízení", "device-details": "Detail zařízení",
"new-device-profile": "Vytvořit nový profil zařízení", "customer-to-assign-device": "Přiřadit zařízení zákazníkovi"
"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"
}, },
"unassign-devices-from-edge-title": "Jste se jisti, že chcete odebrat { count, plural, =1 {1 zařízení} other {# zařízení} }?", "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á." "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", "set-default": "Učinit profil zařízení defaultním",
"delete": "Smazat profil zařízení", "delete": "Smazat profil zařízení",
"copyId": "Kopírovat Id profilu 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": "Název",
"name-required": "Název je povinný.", "name-required": "Název je povinný.",
"type": "Typ profilu", "type": "Typ profilu",

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

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

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

@ -1373,13 +1373,8 @@
"device-configuration": "Device configuration", "device-configuration": "Device configuration",
"transport-configuration": "Transport configuration", "transport-configuration": "Transport configuration",
"wizard": { "wizard": {
"device-wizard": "Device Wizard",
"device-details": "Device details", "device-details": "Device details",
"new-device-profile": "Create new device profile", "customer-to-assign-device": "Customer to assign the device"
"existing-device-profile": "Select existing device profile",
"specific-configuration": "Specific configuration",
"customer-to-assign-device": "Customer to assign the device",
"add-credentials": "Add credentials"
}, },
"unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 device} other {# devices} }?", "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."
@ -1446,8 +1441,6 @@
"delete": "Delete device profile", "delete": "Delete device profile",
"copyId": "Copy device profile Id", "copyId": "Copy device profile Id",
"name-max-length": "Name should be less than 256", "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": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",
"type": "Profile type", "type": "Profile type",

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

@ -1325,13 +1325,8 @@
"device-configuration": "Configuración del dispositivo", "device-configuration": "Configuración del dispositivo",
"transport-configuration": "Configuración del transporte", "transport-configuration": "Configuración del transporte",
"wizard": { "wizard": {
"device-wizard": "Asistente de dispositivo",
"device-details": "Detalles del dispositivo", "device-details": "Detalles del dispositivo",
"new-device-profile": "Crear un nuevo perfil de dispositivo", "customer-to-assign-device": "Cliente al que asignar el 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"
}, },
"unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?", "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." "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", "delete": "Borrar perfil de dispositivo",
"copyId": "Copiar ID de perfil", "copyId": "Copiar ID de perfil",
"name-max-length": "El nombre debe ser menor de 256", "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": "Nombre",
"name-required": "Se requiere nombre.", "name-required": "Se requiere nombre.",
"type": "Tipo de perfil", "type": "Tipo de perfil",

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

@ -1053,13 +1053,8 @@
"device-configuration": "Configuration du dipositif", "device-configuration": "Configuration du dipositif",
"transport-configuration": "Configuration du transport", "transport-configuration": "Configuration du transport",
"wizard": { "wizard": {
"device-wizard": "Wizard du dispositif",
"device-details": "Détails du dispositif", "device-details": "Détails du dispositif",
"new-device-profile": "Créer un nouveau profil de dispositif", "customer-to-assign-device": "Client auquel assigner le 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"
} }
}, },
"device-profile": { "device-profile": {
@ -1079,8 +1074,6 @@
"delete": "Supprimer le profil de dispositif", "delete": "Supprimer le profil de dispositif",
"copyId": "Copier l'Identifiant du profil de dispositif", "copyId": "Copier l'Identifiant du profil de dispositif",
"name-max-length": "La longueur du nom devrait être moins de 256", "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": "Nom",
"name-required": "Nom est requis.", "name-required": "Nom est requis.",
"type": "Type de profile", "type": "Type de profile",

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

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

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

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

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

@ -1021,13 +1021,8 @@
"device-configuration": "Cihaz yapılandırması", "device-configuration": "Cihaz yapılandırması",
"transport-configuration": "Aktarım yapılandırması", "transport-configuration": "Aktarım yapılandırması",
"wizard": { "wizard": {
"device-wizard": "Cihaz Sihirbazı",
"device-details": "Cihaz ayrıntıları", "device-details": "Cihaz ayrıntıları",
"new-device-profile": "Yeni cihaz profili oluştur", "customer-to-assign-device": "Cihazı atamak için kullanıcı grubu"
"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"
}, },
"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-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." "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", "set-default": "Cihaz profilini varsayılan yap",
"delete": "Cihaz profilini sil", "delete": "Cihaz profilini sil",
"copyId": "Cihaz profili kimliğini kopyala", "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": "İsim",
"name-required": "İsim gerekli.", "name-required": "İsim gerekli.",
"type": "Profil türü", "type": "Profil türü",

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

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

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

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

Loading…
Cancel
Save