+
{{ 'device-profile.schedule-day.monday' | translate }}
@@ -64,158 +64,189 @@
device-profile.schedule-time
device-profile.schedule-days
-
-
-
-
- {{ 'device-profile.schedule-day.monday' | translate }}
-
-
-
- device-profile.schedule-time-from
-
-
-
-
-
- device-profile.schedule-time-to
-
-
-
-
-
+
+
+
+ {{ 'device-profile.schedule-day.monday' | translate }}
+
+
+
+ device-profile.schedule-time-from
+
+
+
+
+
+ device-profile.schedule-time-to
+
+
+
+
-
-
- {{ 'device-profile.schedule-day.tuesday' | translate }}
-
-
-
- device-profile.schedule-time-from
-
-
-
-
-
- device-profile.schedule-time-to
-
-
-
-
-
+
-
+
+
+ {{ 'device-profile.schedule-day.tuesday' | translate }}
+
+
+
+ device-profile.schedule-time-from
+
+
+
+
+
+ device-profile.schedule-time-to
+
+
+
+
-
-
-
-
- {{ 'device-profile.schedule-day.friday' | translate }}
-
-
-
- device-profile.schedule-time-from
-
-
-
-
-
- device-profile.schedule-time-to
-
-
-
-
-
+
+
+ {{ 'device-profile.schedule-day.wednesday' | translate }}
+
+
+
+ device-profile.schedule-time-from
+
+
+
+
+
+ device-profile.schedule-time-to
+
+
+
+
+
+
+
+
+
+
+ {{ 'device-profile.schedule-day.thursday' | translate }}
+
+
+
+ device-profile.schedule-time-from
+
+
+
+
+
+ device-profile.schedule-time-to
+
+
+
+
-
-
- {{ 'device-profile.schedule-day.saturday' | translate }}
-
-
-
- device-profile.schedule-time-from
-
-
-
-
-
- device-profile.schedule-time-to
-
-
-
-
-
+
+
+
+
+
+ {{ 'device-profile.schedule-day.friday' | translate }}
+
+
+
+ device-profile.schedule-time-from
+
+
+
+
+
+ device-profile.schedule-time-to
+
+
+
+
+
+
+
+
+
+
+ {{ 'device-profile.schedule-day.saturday' | translate }}
+
+
+
+ device-profile.schedule-time-from
+
+
+
+
+
+ device-profile.schedule-time-to
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.scss
new file mode 100644
index 0000000000..1746a5fbf5
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.scss
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2020 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.
+ */
+::ng-deep {
+ .nowrap {
+ white-space: nowrap;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
index 8cf9dc8d30..0062687d5d 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
@@ -16,6 +16,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
+ AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
@@ -35,6 +36,7 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
@Component({
selector: 'tb-alarm-schedule',
templateUrl: './alarm-schedule.component.html',
+ styleUrls: ['./alarm-schedule.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmScheduleComponent),
@@ -79,7 +81,7 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)))
});
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
- this.alarmScheduleForm.reset({type, items: this.defaultItems}, {emitEvent: false});
+ this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false});
this.updateValidators(type, true);
this.alarmScheduleForm.updateValueAndValidity();
});
@@ -131,13 +133,7 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
this.modelValue.items
.sort((a, b) => a.dayOfWeek - b.dayOfWeek)
.forEach((item, index) => {
- if (item.enabled) {
- this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false});
- this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent: false});
- } else {
- this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false});
- this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent: false});
- }
+ this.disabledSelectedTime(item.enabled, index);
alarmDays.push({
enabled: item.enabled,
startsOn: this.timestampToTime(item.startsOn),
@@ -206,15 +202,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
.filter(day => !!day);
}
if (isDefined(value.startsOn) && value.startsOn !== 0) {
- value.startsOn = this.timeToTimestamp(value.startsOn);
+ value.startsOn = this.timeToTimestampUTC(value.startsOn);
}
if (isDefined(value.endsOn) && value.endsOn !== 0) {
- value.endsOn = this.timeToTimestamp(value.endsOn);
+ value.endsOn = this.timeToTimestampUTC(value.endsOn);
}
if (isDefined(value.items)){
value.items = this.alarmScheduleForm.getRawValue().items;
value.items = value.items.map((item) => {
- return { ...item, startsOn: this.timeToTimestamp(item.startsOn), endsOn: this.timeToTimestamp(item.endsOn)};
+ return { ...item, startsOn: this.timeToTimestampUTC(item.startsOn), endsOn: this.timeToTimestampUTC(item.endsOn)};
});
}
this.modelValue = value;
@@ -222,7 +218,7 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
}
}
- private timeToTimestamp(date: Date | number): number {
+ private timeToTimestampUTC(date: Date | number): number {
if (typeof date === 'number' || date === null) {
return 0;
}
@@ -244,16 +240,39 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
changeCustomScheduler($event: MatCheckboxChange, index: number) {
const value = $event.checked;
- if (value) {
+ this.disabledSelectedTime(value, index, true);
+ }
+
+ private disabledSelectedTime(enable: boolean, index: number, emitEvent = false) {
+ if (enable) {
this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false});
- this.itemsSchedulerForm.at(index).get('endsOn').enable();
+ this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent});
} else {
this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false});
- this.itemsSchedulerForm.at(index).get('endsOn').disable();
+ this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent});
+ }
+ }
+
+ private timeToMoment(date: Date | number): _moment.Moment {
+ if (typeof date === 'number' || date === null) {
+ return _moment([1970, 0, 1, 0, 0, 0, 0]);
+ }
+ return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
+ }
+
+ getSchedulerRangeText(control: FormGroup | AbstractControl): string {
+ const start = this.timeToMoment(control.get('startsOn').value);
+ const end = this.timeToMoment(control.get('endsOn').value);
+ if (start < end) {
+ return `
${start.format('hh:mm A')} – ${end.format('hh:mm A')} `;
+ } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
+ return '
12:00 AM – 12:00 PM ';
}
+ return `
12:00 AM – ${end.format('hh:mm A')} ` +
+ ` and ${start.format('hh:mm A')} – 12:00 PM `;
}
- private get itemsSchedulerForm(): FormArray {
+ get itemsSchedulerForm(): FormArray {
return this.alarmScheduleForm.get('items') as FormArray;
}
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
index 6886e19152..e1883256bb 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
@@ -48,7 +48,7 @@
- device-profile.no-create-alarm-rules
@@ -57,7 +57,7 @@
(click)="addCreateAlarmRule()"
matTooltip="{{ 'device-profile.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
- add_circle_outline
+ add_circle_outline
{{ 'device-profile.add-create-alarm-rule' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss
index b823629076..eedaac2ca1 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss
@@ -16,7 +16,7 @@
:host {
.create-alarm-rule {
- border: 1px groove rgba(0, 0, 0, .25);
+ border: 2px groove rgba(0, 0, 0, .45);
border-radius: 4px;
padding: 8px;
}
@@ -28,4 +28,9 @@
width: 160px;
}
}
+ .button-icon{
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ }
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
index d4a72da751..7d2ad8b75d 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
@@ -18,24 +18,11 @@
+
+
+
+ {{'device-profile.alarm-type' | translate}}
+
+
+ {{ 'device-profile.alarm-type-required' | translate }}
+
+
+
+
+
+
+
+
+
device-profile.advanced-settings
+
+
+
+
+ {{ 'device-profile.propagate-alarm' | translate }}
+
+
+
device-profile.create-alarm-rules
@@ -68,7 +99,7 @@
- device-profile.no-clear-alarm-rule
- add_circle_outline
+ add_circle_outline
{{ 'device-profile.add-clear-alarm-rule' | translate }}
-
-
-
-
-
device-profile.advanced-settings
-
-
-
-
- {{ 'device-profile.propagate-alarm' | translate }}
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss
index 9180f507da..ab28ba6829 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss
@@ -17,7 +17,7 @@
:host {
display: block;
.clear-alarm-rule {
- border: 1px groove rgba(0, 0, 0, .25);
+ border: 2px groove rgba(0, 0, 0, .45);
border-radius: 4px;
padding: 8px;
}
@@ -28,13 +28,16 @@
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
&.mat-expanded {
- height: 80px;
+ height: 48px;
}
}
}
&.advanced-settings {
border: none;
padding: 0;
+ .mat-expansion-panel-header {
+ padding: 0 8px;
+ }
}
}
}
@@ -43,7 +46,7 @@
.mat-expansion-panel {
&.device-profile-alarm {
.mat-expansion-panel-body {
- padding: 0 8px;
+ padding: 0 8px 8px;
}
}
&.advanced-settings {
@@ -51,5 +54,10 @@
padding: 0;
}
}
+ .button-icon{
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ }
}
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss
index a7ac1d1ad6..18883f943b 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@import '../scss/constants';
+@import '../../../../../../scss/constants';
:host {
.tb-device-profile-alarms {
@@ -25,4 +25,8 @@
}
}
}
+
+ .tb-prompt{
+ margin: 30px 0;
+ }
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
index a8af9c2738..9941b1db94 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
@@ -48,7 +48,7 @@
-
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
index c419b7f11c..d7ed08ae9c 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
@@ -19,9 +19,10 @@ import {
ElementRef,
EventEmitter,
forwardRef,
- Input, NgZone,
+ Input,
+ NgZone, OnChanges,
OnInit,
- Output,
+ Output, SimpleChanges,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
@@ -38,14 +39,7 @@ import { TruncatePipe } from '@shared//pipe/truncate.pipe';
import { ENTER } from '@angular/cdk/keycodes';
import { MatDialog } from '@angular/material/dialog';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
-import {
- createDeviceProfileConfiguration,
- createDeviceProfileTransportConfiguration,
- DeviceProfile,
- DeviceProfileInfo,
- DeviceProfileType,
- DeviceTransportType
-} from '@shared/models/device.models';
+import { DeviceProfile, DeviceProfileInfo, DeviceProfileType, DeviceTransportType } from '@shared/models/device.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component';
import { MatAutocomplete } from '@angular/material/autocomplete';
@@ -61,7 +55,7 @@ import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './a
multi: true
}]
})
-export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit {
+export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges {
selectDeviceProfileFormGroup: FormGroup;
@@ -70,12 +64,21 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
@Input()
selectDefaultProfile = false;
+ @Input()
+ selectFirstProfile = false;
+
@Input()
displayAllOnEmpty = false;
@Input()
editProfileEnabled = true;
+ @Input()
+ addNewProfile = true;
+
+ @Input()
+ transportType: DeviceTransportType = null;
+
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
@@ -168,21 +171,59 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
);
}
+ ngOnChanges(changes: SimpleChanges): void {
+ for (const propName of Object.keys(changes)) {
+ const change = changes[propName];
+ if (!change.firstChange && change.currentValue !== change.previousValue) {
+ if (propName === 'transportType') {
+ this.writeValue(null);
+ }
+ }
+ }
+ }
+
selectDefaultDeviceProfileIfNeeded(): void {
if (this.selectDefaultProfile && !this.modelValue) {
this.deviceProfileService.getDefaultDeviceProfileInfo().subscribe(
(profile) => {
- if (profile) {
+ if (profile && (!this.transportType || (profile.transportType === this.transportType))) {
this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false});
this.updateView(profile);
+ } else {
+ this.selectFirstDeviceProfileIfNeeded();
}
}
);
+ } else {
+ this.selectFirstDeviceProfileIfNeeded();
+ }
+ }
+
+ selectFirstDeviceProfileIfNeeded(): void {
+ if (this.selectFirstProfile && !this.modelValue) {
+ const pageLink = new PageLink(1, 0, null, {
+ property: 'createdTime',
+ direction: Direction.DESC
+ });
+ this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).subscribe(
+ (pageData => {
+ const data = pageData.data;
+ if (data.length) {
+ this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(data[0], {emitEvent: false});
+ this.updateView(data[0]);
+ }
+ })
+ );
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
+ if (this.disabled) {
+ this.selectDeviceProfileFormGroup.disable();
+ } else {
+ this.selectDeviceProfileFormGroup.enable();
+ }
}
writeValue(value: DeviceProfileId | null): void {
@@ -244,7 +285,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
property: 'name',
direction: Direction.ASC
});
- return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe(
+ return this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).pipe(
map(pageData => {
let data = pageData.data;
if (this.displayAllOnEmpty) {
@@ -280,9 +321,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
createDeviceProfile($event: Event, profileName: string) {
$event.preventDefault();
const deviceProfile: DeviceProfile = {
- name: profileName
+ name: profileName,
+ transportType: this.transportType
} as DeviceProfile;
- this.openDeviceProfileDialog(deviceProfile, true);
+ if (this.addNewProfile) {
+ this.openDeviceProfileDialog(deviceProfile, true);
+ }
}
editDeviceProfile($event: Event) {
@@ -312,7 +356,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
- deviceProfileName: deviceProfile.name
+ deviceProfileName: deviceProfile.name,
+ transportType: deviceProfile.transportType
}
}).afterClosed();
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html
deleted file mode 100644
index daae838285..0000000000
--- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
-
- device-profile.profile-configuration
-
-
-
-
-
-
-
-
- device-profile.transport-configuration
-
-
-
-
-
-
-
-
- {{'device-profile.alarm-rules' | translate:
- {count: deviceProfileDataFormGroup.get('alarms').value ?
- deviceProfileDataFormGroup.get('alarms').value.length : 0} }}
-
-
-
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts
deleted file mode 100644
index 7d7fc55057..0000000000
--- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-///
-/// Copyright © 2016-2020 The Thingsboard Authors
-///
-/// Licensed under the Apache License, Version 2.0 (the "License");
-/// you may not use this file except in compliance with the License.
-/// You may obtain a copy of the License at
-///
-/// http://www.apache.org/licenses/LICENSE-2.0
-///
-/// Unless required by applicable law or agreed to in writing, software
-/// distributed under the License is distributed on an "AS IS" BASIS,
-/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-/// See the License for the specific language governing permissions and
-/// limitations under the License.
-///
-
-import { Component, forwardRef, Input, OnInit } from '@angular/core';
-import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
-import { Store } from '@ngrx/store';
-import { AppState } from '@app/core/core.state';
-import { coerceBooleanProperty } from '@angular/cdk/coercion';
-import {
- DeviceProfileData,
- DeviceProfileType,
- deviceProfileTypeConfigurationInfoMap,
- DeviceTransportType, deviceTransportTypeConfigurationInfoMap
-} from '@shared/models/device.models';
-
-@Component({
- selector: 'tb-device-profile-data',
- templateUrl: './device-profile-data.component.html',
- styleUrls: [],
- providers: [{
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => DeviceProfileDataComponent),
- multi: true
- }]
-})
-export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit {
-
- deviceProfileDataFormGroup: FormGroup;
-
- private requiredValue: boolean;
- get required(): boolean {
- return this.requiredValue;
- }
- @Input()
- set required(value: boolean) {
- this.requiredValue = coerceBooleanProperty(value);
- }
-
- @Input()
- disabled: boolean;
-
- displayProfileConfiguration: boolean;
- displayTransportConfiguration: boolean;
-
- private propagateChange = (v: any) => { };
-
- constructor(private store: Store
,
- private fb: FormBuilder) {
- }
-
- registerOnChange(fn: any): void {
- this.propagateChange = fn;
- }
-
- registerOnTouched(fn: any): void {
- }
-
- ngOnInit() {
- this.deviceProfileDataFormGroup = this.fb.group({
- configuration: [null, Validators.required],
- transportConfiguration: [null, Validators.required],
- alarms: [null]
- });
- this.deviceProfileDataFormGroup.valueChanges.subscribe(() => {
- this.updateModel();
- });
- }
-
- setDisabledState(isDisabled: boolean): void {
- this.disabled = isDisabled;
- if (this.disabled) {
- this.deviceProfileDataFormGroup.disable({emitEvent: false});
- } else {
- this.deviceProfileDataFormGroup.enable({emitEvent: false});
- }
- }
-
- writeValue(value: DeviceProfileData | null): void {
- const deviceProfileType = value?.configuration?.type;
- this.displayProfileConfiguration = deviceProfileType &&
- deviceProfileTypeConfigurationInfoMap.get(deviceProfileType).hasProfileConfiguration;
- const deviceTransportType = value?.transportConfiguration?.type;
- this.displayTransportConfiguration = deviceTransportType &&
- deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration;
- this.deviceProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false});
- this.deviceProfileDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false});
- this.deviceProfileDataFormGroup.patchValue({alarms: value?.alarms}, {emitEvent: false});
- }
-
- private updateModel() {
- let deviceProfileData: DeviceProfileData = null;
- if (this.deviceProfileDataFormGroup.valid) {
- deviceProfileData = this.deviceProfileDataFormGroup.getRawValue();
- }
- this.propagateChange(deviceProfileData);
- }
-}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
index 8af17883c2..780d98adfe 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
@@ -53,7 +53,7 @@
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
-
+
device-profile.type
@@ -64,21 +64,6 @@
{{ 'device-profile.type-required' | translate }}
-
- device-profile.transport-type
-
-
- {{deviceTransportTypeTranslations.get(type) | translate}}
-
-
-
- {{ 'device-profile.transport-type-required' | translate }}
-
-
-
-
device-profile.description
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
index e3c441c8a2..34c8c7d74c 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
@@ -77,7 +77,11 @@ export class DeviceProfileComponent extends EntityComponent {
name: [entity ? entity.name : '', [Validators.required]],
type: [entity ? entity.type : null, [Validators.required]],
transportType: [entity ? entity.transportType : null, [Validators.required]],
- profileData: [entity && !this.isAdd ? entity.profileData : {}, []],
+ profileData: this.fb.group({
+ configuration: [entity && !this.isAdd ? entity.profileData?.configuration : {}, Validators.required],
+ transportConfiguration: [entity && !this.isAdd ? entity.profileData?.transportConfiguration : {}, Validators.required],
+ alarms: [entity && !this.isAdd ? entity.profileData?.alarms : []]
+ }),
defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []],
description: [entity ? entity.description : '', []],
}
@@ -138,7 +142,7 @@ export class DeviceProfileComponent extends EntityComponent {
if (formValue.defaultRuleChainId) {
formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId);
}
- return formValue;
+ return super.prepareFormValue(formValue);
}
onDeviceProfileIdCopied(event) {
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html
index 200d3d3623..796485d200 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html
@@ -16,9 +16,5 @@
-->
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html
index 94890f3673..061e72c4e4 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.html
@@ -16,9 +16,5 @@
-->
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
index 001502cd83..ea6427addd 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
@@ -29,11 +29,11 @@
formControlName="configuration">
-
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts
index ed81a143fd..7a7a1d285f 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts
@@ -89,7 +89,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
let configuration: DeviceProfileTransportConfiguration = null;
if (this.lwm2mDeviceProfileTransportConfigurationFormGroup.valid) {
configuration = this.lwm2mDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
- configuration.type = DeviceTransportType.LWM2M;
+ // configuration.type = DeviceTransportType.LWM2M;
}
this.propagateChange(configuration);
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
index 659a8156cb..6a964fc524 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
@@ -39,7 +39,7 @@
(blur)="key.isFocused = false; inputChanged(source, key)">
{{key.settings.icon}}
- {{key.settings.requiredErrorMessage}}
+ {{ getErrorMessageText(key.settings, 'required') }}
@@ -53,47 +53,56 @@
[readonly]="key.settings.isEditable === 'readonly'"
type="number"
step="{{key.settings.step}}"
+ min="{{key.settings.minValue}}"
+ max="{{key.settings.maxValue}}"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
{{key.settings.icon}}
- {{key.settings.requiredErrorMessage}}
+ {{ getErrorMessageText(key.settings,'required') }}
+
+
+ {{ getErrorMessageText(key.settings,'min') }}
+
+
+ {{ getErrorMessageText(key.settings,'max') }}
- {{key.label}}
+ {{key.label}}
- {{key.label}}
+ {{key.label}}
@@ -103,12 +112,12 @@
- {{ 'action.undo' | translate }}
+ {{ resetButtonLabel }}
- {{ 'action.save' | translate }}
+ {{ saveButtonLabel }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
index de1a672535..a5afcfabf3 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
@@ -44,7 +44,13 @@
display: block;
margin-top: 20px;
margin-bottom: 16px;
- white-space: normal;
+ .label-wrapper {
+ white-space: normal;
+ }
+ }
+
+ mat-slide-toggle {
+ height: auto;
}
.date-time-input {
@@ -56,20 +62,6 @@
.vertical-alignment {
flex-direction: column;
-
- mat-checkbox,
- mat-slide-toggle {
- margin-top: 18px;
- }
-
- mat-slide-toggle {
- display: flex;
- justify-content: space-between;
- }
- }
-
- .vertically-aligned {
- flex-direction: column;
}
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
index e26600a939..0403f2844c 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
@@ -24,7 +24,7 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
-import { isDefined, isEqual, isUndefined, createLabelFromDatasource } from '@core/utils';
+import { isDefined, isEqual, isUndefined, createLabelFromDatasource, isDefinedAndNotNull } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models';
import * as _moment from 'moment';
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
@@ -47,6 +47,8 @@ interface MultipleInputWidgetSettings {
widgetTitle: string;
showActionButtons: boolean;
updateAllValues: boolean;
+ saveButtonLabel: string;
+ resetButtonLabel: string;
showResultMessage: boolean;
showGroupTitle: boolean;
groupTitle: string;
@@ -62,8 +64,13 @@ interface MultipleInputWidgetDataKeySettings {
isEditable: MultipleInputWidgetDataKeyEditableType;
disabledOnDataKey: string;
dataKeyHidden: boolean;
- step: number;
- requiredErrorMessage: string;
+ step?: number;
+ minValue?: number;
+ maxValue?: number;
+ requiredErrorMessage?: string;
+ invalidDateErrorMessage?: string;
+ minValueErrorMessage?: string;
+ maxValueErrorMessage?: string;
icon: string;
inputTypeNumber?: boolean;
readOnly?: boolean;
@@ -104,7 +111,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
isVerticalAlignment: boolean;
inputWidthSettings: string;
changeAlignment: boolean;
- smallWidthContainer: boolean;
+ saveButtonLabel: string;
+ resetButtonLabel: string;
entityDetected = false;
isAllParametersValid = true;
@@ -157,6 +165,17 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
this.settings.groupTitle = this.settings.groupTitle || '${entityName}';
+ if (this.settings.saveButtonLabel && this.settings.saveButtonLabel.length) {
+ this.saveButtonLabel = this.utils.customTranslation(this.settings.saveButtonLabel, this.settings.saveButtonLabel);
+ } else {
+ this.saveButtonLabel = this.translate.instant('action.save');
+ }
+ if (this.settings.resetButtonLabel && this.settings.resetButtonLabel.length) {
+ this.resetButtonLabel = this.utils.customTranslation(this.settings.resetButtonLabel, this.settings.resetButtonLabel);
+ } else {
+ this.resetButtonLabel = this.translate.instant('action.undo');
+ }
+
// For backward compatibility
if (isUndefined(this.settings.showActionButtons)) {
this.settings.showActionButtons = true;
@@ -195,7 +214,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
if (dataKey.units) {
dataKey.label += ' (' + dataKey.units + ')';
}
- dataKey.formId = (++keyIndex)+'';
+ dataKey.formId = (++keyIndex) + '';
dataKey.isFocused = false;
// For backward compatibility
@@ -245,6 +264,14 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
if (key.settings.dataKeyValueType === 'integer') {
validators.push(Validators.pattern(/^-?[0-9]+$/));
}
+ if (key.settings.dataKeyValueType === 'integer' || key.settings.dataKeyValueType === 'double') {
+ if (isDefinedAndNotNull(key.settings.minValue) && isFinite(key.settings.minValue)) {
+ validators.push(Validators.min(key.settings.minValue));
+ }
+ if (isDefinedAndNotNull(key.settings.maxValue) && isFinite(key.settings.maxValue)) {
+ validators.push(Validators.max(key.settings.maxValue));
+ }
+ }
const formControl = this.fb.control(
{ value: key.value,
disabled: key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition},
@@ -265,7 +292,11 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
switch (key.settings.dataKeyValueType) {
case 'dateTime':
case 'date':
- value = _moment(keyData[0][1]).toDate();
+ if (isDefinedAndNotNull(keyData[0][1]) && keyData[0][1] !== '') {
+ value = _moment(keyData[0][1]).toDate();
+ } else {
+ value = null;
+ }
break;
case 'time':
value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate();
@@ -313,25 +344,69 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
private updateWidgetDisplaying() {
this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620);
- this.smallWidthContainer = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 420);
}
public onDataUpdated() {
- this.ngZone.run(() => {
- this.updateWidgetData(this.subscription.data);
- this.ctx.detectChanges();
- });
+ this.updateWidgetData(this.subscription.data);
+ this.ctx.detectChanges();
}
private resize() {
- this.ngZone.run(() => {
- this.updateWidgetDisplaying();
- this.ctx.detectChanges();
- });
+ this.updateWidgetDisplaying();
+ this.ctx.detectChanges();
}
public getGroupTitle(datasource: Datasource): string {
- return createLabelFromDatasource(datasource, this.settings.groupTitle);
+ const groupTitle = createLabelFromDatasource(datasource, this.settings.groupTitle);
+ return this.utils.customTranslation(groupTitle, groupTitle);
+ }
+
+ public getErrorMessageText(keySettings: MultipleInputWidgetDataKeySettings, errorType: string): string {
+ let errorMessage;
+ let defaultMessage;
+ let messageValues;
+ switch (errorType) {
+ case 'required':
+ errorMessage = keySettings.requiredErrorMessage;
+ defaultMessage = '';
+ break;
+ case 'min':
+ errorMessage = keySettings.minValueErrorMessage;
+ defaultMessage = 'widgets.input-widgets.min-value-error';
+ messageValues = {
+ value: keySettings.minValue
+ };
+ break;
+ case 'max':
+ errorMessage = keySettings.maxValueErrorMessage;
+ defaultMessage = 'widgets.input-widgets.max-value-error';
+ messageValues = {
+ value: keySettings.maxValue
+ };
+ break;
+ case 'invalidDate':
+ errorMessage = keySettings.invalidDateErrorMessage;
+ defaultMessage = 'widgets.input-widgets.invalid-date';
+ break;
+ default:
+ return '';
+ }
+ return this.getTranslatedErrorText(errorMessage, defaultMessage, messageValues);
+ }
+
+ public getTranslatedErrorText(errorMessage: string, defaultMessage: string, messageValues?: object): string {
+ let messageText;
+ if (errorMessage && errorMessage.length) {
+ messageText = this.utils.customTranslation(errorMessage, errorMessage);
+ } else if (defaultMessage && defaultMessage.length) {
+ if (!messageValues) {
+ messageValues = {};
+ }
+ messageText = this.translate.instant(defaultMessage, messageValues);
+ } else {
+ messageText = '';
+ }
+ return messageText;
}
public visibleKeys(source: MultipleInputWidgetSource): MultipleInputWidgetDataKey[] {
@@ -455,8 +530,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
if (tasks.length) {
forkJoin(tasks).subscribe(
() => {
- this.multipleInputFormGroup.reset(undefined, {emitEvent: false});
this.multipleInputFormGroup.markAsPristine();
+ this.ctx.detectChanges();
if (this.settings.showResultMessage) {
this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'),
1000, 'bottom', 'left', this.toastTargetId);
@@ -469,8 +544,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}
});
} else {
- this.multipleInputFormGroup.reset(undefined, {emitEvent: false});
this.multipleInputFormGroup.markAsPristine();
+ this.ctx.detectChanges();
}
}
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
new file mode 100644
index 0000000000..1e59f0c9e1
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
@@ -0,0 +1,170 @@
+
+
+
+ device.add-device-text
+
+
+ close
+
+
+
+
+
+
+
+
+ check
+
+
+
+
+
+
+ {{ 'device-profile.transport-configuration' | translate }}
+
+
+
+
+
+
+ {{'device-profile.alarm-rules-with-count' | translate:
+ {count: alarmRulesFormGroup.get('alarms').value ?
+ alarmRulesFormGroup.get('alarms').value.length : 0} }}
+
+
+
+
+
+ {{ 'device.credentials' | translate }}
+
+ {{ 'device.wizard.add-credential' | translate }}
+
+
+
+
+
+ {{ 'customer.customer' | translate }}
+
+
+
+
+
+
+
+
+
+ {{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}
+
+
+
{{ 'action.cancel' | translate }}
+
+
+ 0"
+ [disabled]="(isLoading$ | async)"
+ (click)="previousStep()">{{ 'action.back' | translate }}
+ {{ 'action.add' | translate }}
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
new file mode 100644
index 0000000000..1426e0f201
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
@@ -0,0 +1,64 @@
+/**
+ * Copyright © 2016-2020 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-context(.tb-fullscreen-dialog .mat-dialog-container) {
+ @media #{$mat-lt-sm} {
+ .mat-dialog-content {
+ max-height: 75vh;
+ }
+ }
+}
+
+:host ::ng-deep {
+ .mat-dialog-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ .mat-stepper-horizontal {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ @media #{$mat-lt-sm} {
+ .mat-step-label {
+ white-space: normal;
+ overflow: visible;
+ .mat-step-text-label {
+ overflow: visible;
+ }
+ }
+ }
+ .mat-horizontal-content-container {
+ height: 450px;
+ max-height: 100%;
+ width: 100%;;
+ overflow-y: auto;
+ @media #{$mat-gt-sm} {
+ min-width: 800px;
+ }
+ }
+ .mat-horizontal-stepper-content[aria-expanded=true] {
+ height: 100%;
+ form {
+ height: 100%;
+ }
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
new file mode 100644
index 0000000000..9e861c1359
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
@@ -0,0 +1,331 @@
+///
+/// Copyright © 2016-2020 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
+import { DialogComponent } from '@shared/components/dialog.component';
+import { Router } from '@angular/router';
+import {
+ createDeviceProfileConfiguration,
+ createDeviceProfileTransportConfiguration,
+ DeviceProfile,
+ DeviceProfileType,
+ DeviceTransportType, deviceTransportTypeConfigurationInfoMap, deviceTransportTypeHintMap,
+ deviceTransportTypeTranslationMap
+} from '@shared/models/device.models';
+import { MatHorizontalStepper } from '@angular/material/stepper';
+import { AddEntityDialogData } from '@home/models/entity/entity-component.models';
+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 } from 'rxjs';
+import { map, mergeMap, tap } 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 { MediaBreakpoints } from '@shared/models/constants';
+
+@Component({
+ selector: 'tb-device-wizard',
+ templateUrl: './device-wizard-dialog.component.html',
+ providers: [],
+ styleUrls: ['./device-wizard-dialog.component.scss']
+})
+export class DeviceWizardDialogComponent extends
+ DialogComponent
implements OnDestroy, ErrorStateMatcher {
+
+ @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatHorizontalStepper;
+
+ selectedIndex = 0;
+
+ showNext = true;
+
+ createProfile = false;
+ createTransportConfiguration = false;
+
+ entityType = EntityType;
+
+ deviceTransportTypes = Object.keys(DeviceTransportType);
+
+ deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
+
+ deviceTransportTypeHints = deviceTransportTypeHintMap;
+
+ deviceWizardFormGroup: FormGroup;
+
+ transportConfigFormGroup: FormGroup;
+
+ alarmRulesFormGroup: FormGroup;
+
+ credentialsFormGroup: FormGroup;
+
+ customerFormGroup: FormGroup;
+
+ labelPosition = 'end';
+
+ private subscriptions: Subscription[] = [];
+
+ constructor(protected store: Store,
+ protected router: Router,
+ @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData>,
+ @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
+ public dialogRef: MatDialogRef,
+ private deviceProfileService: DeviceProfileService,
+ private deviceService: DeviceService,
+ private breakpointObserver: BreakpointObserver,
+ private fb: FormBuilder) {
+ super(store, router, dialogRef);
+ this.deviceWizardFormGroup = this.fb.group({
+ name: ['', Validators.required],
+ label: [''],
+ gateway: [false],
+ transportType: [DeviceTransportType.DEFAULT, Validators.required],
+ addProfileType: [0],
+ deviceProfileId: [null, Validators.required],
+ newDeviceProfileTitle: [{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.updateValueAndValidity();
+ this.createProfile = false;
+ this.createTransportConfiguration = 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.updateValueAndValidity();
+ this.createProfile = true;
+ this.createTransportConfiguration = this.deviceWizardFormGroup.get('transportType').value &&
+ deviceTransportTypeConfigurationInfoMap.get(this.deviceWizardFormGroup.get('transportType').value).hasProfileConfiguration;
+ }
+ }
+ ));
+
+ this.transportConfigFormGroup = this.fb.group(
+ {
+ transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required]
+ }
+ );
+ this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => {
+ this.deviceProfileTransportTypeChanged(transportType);
+ }));
+
+ this.alarmRulesFormGroup = this.fb.group({
+ alarms: [null]
+ }
+ );
+
+ 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]
+ }
+ );
+
+ 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: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
+ const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
+ const customErrorState = !!(control && control.invalid);
+ return originalErrorState || customErrorState;
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ previousStep(): void {
+ this.addDeviceWizardStepper.previous();
+ }
+
+ nextStep(): void {
+ this.addDeviceWizardStepper.next();
+ }
+
+ getFormLabel(index: number): string {
+ if (index > 0) {
+ if (!this.createProfile) {
+ index += 2;
+ } else if (!this.createTransportConfiguration) {
+ index += 1;
+ }
+ }
+ 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.credentials';
+ case 4:
+ return 'customer.customer';
+ }
+ }
+
+ get maxStepperIndex(): number {
+ return this.addDeviceWizardStepper?._steps?.length - 1;
+ }
+
+ private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void {
+ this.transportConfigFormGroup.patchValue(
+ {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)});
+ this.createTransportConfiguration = this.createProfile && deviceTransportType &&
+ deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration;
+ }
+
+ add(): void {
+ if (this.allValid()) {
+ this.createDeviceProfile().pipe(
+ mergeMap(profileId => this.createDevice(profileId)),
+ mergeMap(device => this.saveCredentials(device))
+ ).subscribe(
+ (created) => {
+ this.dialogRef.close(created);
+ }
+ );
+ }
+ }
+
+ private createDeviceProfile(): Observable {
+ if (this.deviceWizardFormGroup.get('addProfileType').value) {
+ const deviceProfile: DeviceProfile = {
+ name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value,
+ type: DeviceProfileType.DEFAULT,
+ transportType: this.deviceWizardFormGroup.get('transportType').value,
+ profileData: {
+ configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
+ transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
+ alarms: this.alarmRulesFormGroup.get('alarms').value
+ }
+ };
+ return this.deviceProfileService.saveDeviceProfile(deviceProfile).pipe(
+ map(profile => profile.id),
+ tap((profileId) => {
+ this.deviceWizardFormGroup.patchValue({
+ deviceProfileId: profileId,
+ addProfileType: 0
+ });
+ })
+ );
+ } else {
+ return of(null);
+ }
+ }
+
+ private createDevice(profileId: EntityId = this.deviceWizardFormGroup.get('deviceProfileId').value): Observable> {
+ const device = {
+ name: this.deviceWizardFormGroup.get('name').value,
+ label: this.deviceWizardFormGroup.get('label').value,
+ deviceProfileId: profileId,
+ additionalInfo: {
+ gateway: this.deviceWizardFormGroup.get('gateway').value,
+ description: this.deviceWizardFormGroup.get('description').value
+ },
+ customerId: null
+ };
+ if (this.customerFormGroup.get('customerId').value) {
+ device.customerId = {
+ entityType: EntityType.CUSTOMER,
+ id: this.customerFormGroup.get('customerId').value
+ };
+ }
+ return this.data.entitiesTableConfig.saveEntity(device);
+ }
+
+ private saveCredentials(device: BaseData): Observable {
+ 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);
+ }
+ ),
+ map(() => true));
+ }
+ return of(true);
+ }
+
+ allValid(): boolean {
+ if (this.addDeviceWizardStepper.steps.find((item, index) => {
+ if (item.stepControl.invalid) {
+ item.interacted = true;
+ this.addDeviceWizardStepper.selectedIndex = index;
+ return true;
+ } 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;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
index 47f1a5261b..af7dc98e19 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
+++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
@@ -22,6 +22,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum';
import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
+import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component';
const routes: Routes = [
{
@@ -77,6 +78,19 @@ const routes: Routes = [
icon: 'security'
}
}
+ },
+ {
+ path: 'oauth2',
+ component: OAuth2SettingsComponent,
+ canDeactivate: [ConfirmOnExitGuard],
+ data: {
+ auth: [Authority.SYS_ADMIN],
+ title: 'admin.oauth2.oauth2',
+ breadcrumb: {
+ label: 'admin.oauth2.oauth2',
+ icon: 'security'
+ }
+ }
}
]
}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
index 48bcf56fd5..73fef6a32f 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
+++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
@@ -23,13 +23,15 @@ import { MailServerComponent } from '@modules/home/pages/admin/mail-server.compo
import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
+import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
@NgModule({
declarations:
[
GeneralSettingsComponent,
MailServerComponent,
- SecuritySettingsComponent
+ SecuritySettingsComponent,
+ OAuth2SettingsComponent
],
imports: [
CommonModule,
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html
new file mode 100644
index 0000000000..30564e9306
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html
@@ -0,0 +1,478 @@
+
+
+
+
+
+
admin.oauth2.oauth2
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'admin.oauth2.enable' | translate }}
+
+
+
+
+
+
+
+
+
+ {{ domainListTittle(domain) }}
+
+
+
+ delete
+
+
+
+
+
+
+
+
+
+ {{'admin.oauth2.add-domain' | translate}}
+
+
+
+
+
+
+
+
+
+ {{ getProviderName(registration) }}
+
+
+
+ delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{'admin.oauth2.add-provider' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ add
+ action.add
+
+
+ {{'action.save' | translate}}
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss
new file mode 100644
index 0000000000..0ad644f61b
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2020 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{
+ .checkbox-row {
+ margin-top: 1em;
+ }
+
+ .registration-card{
+ margin-bottom: 0.5em;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+
+ .custom-settings{
+ font-size: 16px;
+ .mat-expansion-panel-header{
+ padding: 0 2px;
+ }
+ }
+ }
+
+ .container{
+ margin-bottom: 1em;
+
+ .tb-highlight{
+ margin: 0;
+ }
+
+ .tb-hint{
+ padding-bottom: 0;
+ }
+ }
+}
+
+:host ::ng-deep{
+ .registration-card{
+ .custom-settings{
+ .mat-expansion-panel-body{
+ padding: 0 2px 1em;
+ }
+ .mat-tab-label{
+ text-transform: none;
+ }
+ .mat-form-field-suffix .mat-icon-button .mat-icon{
+ font-size: 18px;
+ }
+ }
+ }
+ .domains-list{
+ margin-bottom: 1.5em;
+ .mat-form-field-suffix .mat-icon-button .mat-icon{
+ font-size: 24px;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts
new file mode 100644
index 0000000000..11b39468e9
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts
@@ -0,0 +1,500 @@
+///
+/// Copyright © 2016-2020 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
+import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import {
+ ClientAuthenticationMethod,
+ ClientProviderTemplated,
+ ClientRegistration,
+ DomainInfo,
+ DomainSchema,
+ domainSchemaTranslations,
+ DomainsParam,
+ MapperConfig,
+ MapperConfigBasic,
+ MapperConfigCustom,
+ MapperConfigType,
+ OAuth2Settings,
+ TenantNameStrategy
+} from '@shared/models/settings.models';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { AdminService } from '@core/http/admin.service';
+import { PageComponent } from '@shared/components/page.component';
+import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
+import { COMMA, ENTER } from '@angular/cdk/keycodes';
+import { MatChipInputEvent } from '@angular/material/chips';
+import { WINDOW } from '@core/services/window.service';
+import { forkJoin, Subscription } from 'rxjs';
+import { DialogService } from '@core/services/dialog.service';
+import { TranslateService } from '@ngx-translate/core';
+import { isDefined } from '@core/utils';
+
+@Component({
+ selector: 'tb-oauth2-settings',
+ templateUrl: './oauth2-settings.component.html',
+ styleUrls: ['./oauth2-settings.component.scss', './settings-card.scss']
+})
+export class OAuth2SettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
+
+ private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/;
+ private subscriptions: Subscription[] = [];
+ private templates = new Map();
+ private defaultProvider = {
+ additionalInfo: {
+ providerName: 'Custom'
+ },
+ clientAuthenticationMethod: ClientAuthenticationMethod.POST,
+ userNameAttributeName: 'email',
+ mapperConfig: {
+ allowUserCreation: true,
+ activateUser: false,
+ type: MapperConfigType.BASIC,
+ basic: {
+ emailAttributeKey: 'email',
+ tenantNameStrategy: TenantNameStrategy.DOMAIN,
+ alwaysFullScreen: false
+ }
+ }
+ };
+
+ readonly separatorKeysCodes: number[] = [ENTER, COMMA];
+
+ oauth2SettingsForm: FormGroup;
+ oauth2Settings: OAuth2Settings;
+
+ clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod);
+ converterTypesExternalUser = Object.keys(MapperConfigType);
+ tenantNameStrategies = Object.keys(TenantNameStrategy);
+ protocols = Object.keys(DomainSchema);
+ domainSchemaTranslations = domainSchemaTranslations;
+
+ templateProvider = ['Custom'];
+
+ constructor(protected store: Store,
+ private adminService: AdminService,
+ private fb: FormBuilder,
+ private dialogService: DialogService,
+ private translate: TranslateService,
+ @Inject(WINDOW) private window: Window) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.buildOAuth2SettingsForm();
+ forkJoin([
+ this.adminService.getOAuth2Template(),
+ this.adminService.getOAuth2Settings()
+ ]).subscribe(
+ ([templates, oauth2Settings]) => {
+ this.initTemplates(templates);
+ this.oauth2Settings = oauth2Settings;
+ this.initOAuth2Settings(this.oauth2Settings);
+ }
+ );
+ }
+
+ ngOnDestroy() {
+ super.ngOnDestroy();
+ this.subscriptions.forEach((subscription) => {
+ subscription.unsubscribe();
+ });
+ }
+
+ private initTemplates(templates: ClientProviderTemplated[]): void {
+ templates.map(provider => this.templates.set(provider.name, provider));
+ this.templateProvider.push(...Array.from(this.templates.keys()));
+ this.templateProvider.sort();
+ }
+
+ get domainsParams(): FormArray {
+ return this.oauth2SettingsForm.get('domainsParams') as FormArray;
+ }
+
+ private formBasicGroup(mapperConfigBasic?: MapperConfigBasic): FormGroup {
+ let tenantNamePattern;
+ if (mapperConfigBasic?.tenantNamePattern) {
+ tenantNamePattern = mapperConfigBasic.tenantNamePattern;
+ } else {
+ tenantNamePattern = {value: null, disabled: true};
+ }
+ const basicGroup = this.fb.group({
+ emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', Validators.required],
+ firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : ''],
+ lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : ''],
+ tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN],
+ tenantNamePattern: [tenantNamePattern, Validators.required],
+ customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null],
+ defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null],
+ alwaysFullScreen: [mapperConfigBasic?.alwaysFullScreen ? mapperConfigBasic.alwaysFullScreen : false]
+ });
+
+ this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => {
+ if (domain === 'CUSTOM') {
+ basicGroup.get('tenantNamePattern').enable();
+ } else {
+ basicGroup.get('tenantNamePattern').disable();
+ }
+ }));
+
+ return basicGroup;
+ }
+
+ private formCustomGroup(mapperConfigCustom?: MapperConfigCustom): FormGroup {
+ return this.fb.group({
+ url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
+ username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null],
+ password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null]
+ });
+ }
+
+ private buildOAuth2SettingsForm(): void {
+ this.oauth2SettingsForm = this.fb.group({
+ domainsParams: this.fb.array([]),
+ enabled: [false]
+ });
+ }
+
+ private initOAuth2Settings(oauth2Settings: OAuth2Settings): void {
+ if (oauth2Settings) {
+ this.oauth2SettingsForm.patchValue({enabled: oauth2Settings.enabled}, {emitEvent: false});
+ oauth2Settings.domainsParams.forEach((domain) => {
+ this.domainsParams.push(this.buildDomainsForm(domain));
+ });
+ }
+ }
+
+ private uniqueDomainValidator(control: FormGroup): { [key: string]: boolean } | null {
+ if (control.parent?.value) {
+ const domain = control.value.name;
+ const listProtocols = control.parent.getRawValue()
+ .filter((domainInfo) => domainInfo.name === domain)
+ .map((domainInfo) => domainInfo.scheme);
+ if (listProtocols.length > 1 && listProtocols.indexOf(DomainSchema.MIXED) > -1 ||
+ new Set(listProtocols).size !== listProtocols.length) {
+ return {unique: true};
+ }
+ }
+ return null;
+ }
+
+ public domainListTittle(control: AbstractControl): string {
+ const domainInfos = control.get('domainInfos').value as DomainInfo[];
+ if (domainInfos.length) {
+ const domainList = new Set();
+ domainInfos.forEach((domain) => {
+ domainList.add(domain.name);
+ });
+ return Array.from(domainList).join(', ');
+ }
+ return this.translate.instant('admin.oauth2.new-domain');
+ }
+
+ private buildDomainsForm(domainParams?: DomainsParam): FormGroup {
+ const formDomain = this.fb.group({
+ domainInfos: this.fb.array([], Validators.required),
+ clientRegistrations: this.fb.array([], Validators.required)
+ });
+
+ if (domainParams) {
+ domainParams.domainInfos.forEach((domain) => {
+ this.clientDomainInfos(formDomain).push(this.buildDomainForm(domain));
+ });
+ domainParams.clientRegistrations.forEach((registration) => {
+ this.clientDomainProviders(formDomain).push(this.buildProviderForm(registration));
+ });
+ } else {
+ this.clientDomainProviders(formDomain).push(this.buildProviderForm());
+ this.clientDomainInfos(formDomain).push(this.buildDomainForm());
+ }
+
+ return formDomain;
+ }
+
+ private buildDomainForm(domainInfo?: DomainInfo): FormGroup {
+ const domain = this.fb.group({
+ name: [domainInfo ? domainInfo.name : this.window.location.hostname, [
+ Validators.required,
+ Validators.pattern('((?![:/]).)*$')]],
+ scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required]
+ }, {validators: this.uniqueDomainValidator});
+ return domain;
+ }
+
+ private buildProviderForm(registrationData?: ClientRegistration): FormGroup {
+ let additionalInfo = null;
+ if (registrationData?.additionalInfo) {
+ additionalInfo = JSON.parse(registrationData.additionalInfo);
+ if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) {
+ additionalInfo.providerName = 'Custom';
+ }
+ }
+ let defaultProviderName = 'Custom';
+ if (this.templateProvider.indexOf('Google')) {
+ defaultProviderName = 'Google';
+ }
+
+ const clientRegistration = this.fb.group({
+ id: this.fb.group({
+ id: [registrationData?.id?.id ? registrationData.id.id : null],
+ entityType: [registrationData?.id?.entityType ? registrationData.id.entityType : null]
+ }),
+ additionalInfo: this.fb.group({
+ providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required]
+ }),
+ loginButtonLabel: [registrationData?.loginButtonLabel ? registrationData.loginButtonLabel : null, Validators.required],
+ loginButtonIcon: [registrationData?.loginButtonIcon ? registrationData.loginButtonIcon : null],
+ clientId: [registrationData?.clientId ? registrationData.clientId : '', Validators.required],
+ clientSecret: [registrationData?.clientSecret ? registrationData.clientSecret : '', Validators.required],
+ accessTokenUri: [registrationData?.accessTokenUri ? registrationData.accessTokenUri : '',
+ [Validators.required,
+ Validators.pattern(this.URL_REGEXP)]],
+ authorizationUri: [registrationData?.authorizationUri ? registrationData.authorizationUri : '',
+ [Validators.required,
+ Validators.pattern(this.URL_REGEXP)]],
+ scope: this.fb.array(registrationData?.scope ? registrationData.scope : [], Validators.required),
+ jwkSetUri: [registrationData?.jwkSetUri ? registrationData.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)],
+ userInfoUri: [registrationData?.userInfoUri ? registrationData.userInfoUri : '',
+ [Validators.required,
+ Validators.pattern(this.URL_REGEXP)]],
+ clientAuthenticationMethod: [
+ registrationData?.clientAuthenticationMethod ? registrationData.clientAuthenticationMethod : ClientAuthenticationMethod.POST,
+ Validators.required],
+ userNameAttributeName: [
+ registrationData?.userNameAttributeName ? registrationData.userNameAttributeName : 'email', Validators.required],
+ mapperConfig: this.fb.group({
+ allowUserCreation: [registrationData?.mapperConfig?.allowUserCreation ? registrationData.mapperConfig.allowUserCreation : true],
+ activateUser: [registrationData?.mapperConfig?.activateUser ? registrationData.mapperConfig.activateUser : false],
+ type: [registrationData?.mapperConfig?.type ? registrationData.mapperConfig.type : MapperConfigType.BASIC, Validators.required]
+ }
+ )
+ });
+
+ if (registrationData) {
+ this.changeMapperConfigType(clientRegistration, registrationData.mapperConfig.type, registrationData.mapperConfig);
+ } else {
+ this.changeMapperConfigType(clientRegistration, MapperConfigType.BASIC);
+ this.setProviderDefaultValue(defaultProviderName, clientRegistration);
+ }
+
+ this.subscriptions.push(clientRegistration.get('mapperConfig.type').valueChanges.subscribe((value) => {
+ this.changeMapperConfigType(clientRegistration, value);
+ }));
+
+ this.subscriptions.push(clientRegistration.get('additionalInfo.providerName').valueChanges.subscribe((provider) => {
+ (clientRegistration.get('scope') as FormArray).clear();
+ this.setProviderDefaultValue(provider, clientRegistration);
+ }));
+
+ return clientRegistration;
+ }
+
+ private setProviderDefaultValue(provider: string, clientRegistration: FormGroup) {
+ if (provider === 'Custom') {
+ const defaultSettings = {...this.defaultProvider, ...{id: clientRegistration.get('id').value}};
+ clientRegistration.reset(defaultSettings, {emitEvent: false});
+ clientRegistration.get('accessTokenUri').enable();
+ clientRegistration.get('authorizationUri').enable();
+ clientRegistration.get('jwkSetUri').enable();
+ clientRegistration.get('userInfoUri').enable();
+ } else {
+ const template = this.templates.get(provider);
+ delete template.id;
+ delete template.additionalInfo;
+ template.clientId = '';
+ template.clientSecret = '';
+ template.scope.forEach(() => {
+ (clientRegistration.get('scope') as FormArray).push(this.fb.control(''));
+ });
+ clientRegistration.get('accessTokenUri').disable();
+ clientRegistration.get('authorizationUri').disable();
+ clientRegistration.get('jwkSetUri').disable();
+ clientRegistration.get('userInfoUri').disable();
+ clientRegistration.patchValue(template, {emitEvent: false});
+ }
+ }
+
+ private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) {
+ const mapperConfig = control.get('mapperConfig') as FormGroup;
+ if (type === MapperConfigType.BASIC) {
+ mapperConfig.removeControl('custom');
+ mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic));
+ } else {
+ mapperConfig.removeControl('basic');
+ mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom));
+ }
+ }
+
+ save(): void {
+ const setting = this.prepareFormValue(this.oauth2SettingsForm.getRawValue());
+ this.adminService.saveOAuth2Settings(setting).subscribe(
+ (oauth2Settings) => {
+ this.oauth2Settings = oauth2Settings;
+ this.oauth2SettingsForm.markAsPristine();
+ this.oauth2SettingsForm.markAsUntouched();
+ }
+ );
+ }
+
+ private prepareFormValue(formValue: OAuth2Settings): OAuth2Settings{
+ formValue.domainsParams.forEach((setting, index) => {
+ setting.clientRegistrations.forEach((registration) => {
+ registration.additionalInfo = JSON.stringify(registration.additionalInfo);
+ if (registration.id.id === null) {
+ delete registration.id;
+ }
+ });
+ });
+ return formValue;
+ }
+
+ confirmForm(): FormGroup {
+ return this.oauth2SettingsForm;
+ }
+
+ addScope(event: MatChipInputEvent, control: AbstractControl): void {
+ const input = event.input;
+ const value = event.value;
+ const controller = control.get('scope') as FormArray;
+ if ((value.trim() !== '')) {
+ controller.push(this.fb.control(value.trim()));
+ controller.markAsDirty();
+ }
+
+ if (input) {
+ input.value = '';
+ }
+ }
+
+ removeScope(i: number, control: AbstractControl): void {
+ const controller = control.get('scope') as FormArray;
+ controller.removeAt(i);
+ controller.markAsTouched();
+ controller.markAsDirty();
+ }
+
+ addDomain(): void {
+ this.domainsParams.push(this.buildDomainsForm());
+ }
+
+ deleteDomain($event: Event, index: number): void {
+ if ($event) {
+ $event.stopPropagation();
+ $event.preventDefault();
+ }
+
+ const domainName = this.domainListTittle(this.domainsParams.at(index));
+ this.dialogService.confirm(
+ this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domainName || ''}),
+ this.translate.instant('admin.oauth2.delete-domain-text'), null,
+ this.translate.instant('action.delete')
+ ).subscribe((data) => {
+ if (data) {
+ this.domainsParams.removeAt(index);
+ this.domainsParams.markAsTouched();
+ this.domainsParams.markAsDirty();
+ }
+ });
+ }
+
+ clientDomainProviders(control: AbstractControl): FormArray {
+ return control.get('clientRegistrations') as FormArray;
+ }
+
+ clientDomainInfos(control: AbstractControl): FormArray {
+ return control.get('domainInfos') as FormArray;
+ }
+
+ addProvider(control: AbstractControl): void {
+ this.clientDomainProviders(control).push(this.buildProviderForm());
+ }
+
+ deleteProvider($event: Event, control: AbstractControl, index: number): void {
+ if ($event) {
+ $event.stopPropagation();
+ $event.preventDefault();
+ }
+
+ const providerName = this.clientDomainProviders(control).at(index).get('additionalInfo.providerName').value;
+ this.dialogService.confirm(
+ this.translate.instant('admin.oauth2.delete-registration-title', {name: providerName || ''}),
+ this.translate.instant('admin.oauth2.delete-registration-text'), null,
+ this.translate.instant('action.delete')
+ ).subscribe((data) => {
+ if (data) {
+ this.clientDomainProviders(control).removeAt(index);
+ this.clientDomainProviders(control).markAsTouched();
+ this.clientDomainProviders(control).markAsDirty();
+ }
+ });
+ }
+
+ toggleEditMode(control: AbstractControl, path: string) {
+ control.get(path).disabled ? control.get(path).enable() : control.get(path).disable();
+ }
+
+ getProviderName(controller: AbstractControl): string {
+ return controller.get('additionalInfo.providerName').value;
+ }
+
+ getHelpLink(controller: AbstractControl): string {
+ const provider = controller.get('additionalInfo.providerName').value;
+ if (provider === null || provider === 'Custom') {
+ return '';
+ }
+ return this.templates.get(provider).helpLink;
+ }
+
+ addDomainInfo(control: AbstractControl): void {
+ this.clientDomainInfos(control).push(this.buildDomainForm({
+ name: '',
+ scheme: DomainSchema.HTTPS
+ }));
+ }
+
+ removeDomain($event: Event, control: AbstractControl, index: number): void {
+ if ($event) {
+ $event.stopPropagation();
+ $event.preventDefault();
+ }
+ this.clientDomainInfos(control).removeAt(index);
+ this.clientDomainInfos(control).markAsTouched();
+ this.clientDomainInfos(control).markAsDirty();
+ }
+
+ redirectURI(control: AbstractControl, schema?: DomainSchema): string {
+ const domainInfo = control.value as DomainInfo;
+ if (domainInfo.name !== '') {
+ let protocol;
+ if (isDefined(schema)) {
+ protocol = schema.toLowerCase();
+ } else {
+ protocol = domainInfo.scheme === DomainSchema.MIXED ? DomainSchema.HTTPS.toLowerCase() : domainInfo.scheme.toLowerCase();
+ }
+ return `${protocol}://${domainInfo.name}/login/oauth2/code/`;
+ }
+ return '';
+ }
+
+ redirectURIMixed(control: AbstractControl): string {
+ return this.redirectURI(control, DomainSchema.HTTP);
+ }
+
+ trackByParams(index: number): number {
+ return index;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
index 40a9f55aa5..437fc21c86 100644
--- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
+++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
@@ -16,6 +16,52 @@
-->
+
+
+ device-profile.transport-type
+
+
+ {{deviceTransportTypeTranslations.get(type) | translate}}
+
+
+
+ {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}}
+
+
+ {{ 'device-profile.transport-type-required' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
index 9cf18c498e..cba34b4987 100644
--- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
@@ -18,7 +18,12 @@ import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
-import { DeviceProfile } from '@shared/models/device.models';
+import {
+ DeviceProfile,
+ DeviceTransportType,
+ deviceTransportTypeHintMap,
+ deviceTransportTypeTranslationMap
+} from '@shared/models/device.models';
@Component({
selector: 'tb-device-profile-tabs',
@@ -27,6 +32,12 @@ import { DeviceProfile } from '@shared/models/device.models';
})
export class DeviceProfileTabsComponent extends EntityTabsComponent {
+ deviceTransportTypes = Object.keys(DeviceTransportType);
+
+ deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
+
+ deviceTransportTypeHints = deviceTransportTypeHintMap;
+
constructor(protected store: Store) {
super(store);
}
diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
index c4927fbe99..60cb15c199 100644
--- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
+++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
@@ -59,6 +59,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve
-
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html
index cd2d53aab0..68189fbb84 100644
--- a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html
@@ -16,9 +16,9 @@
-->
-
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html
index f109335edc..6762202f06 100644
--- a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html
@@ -29,11 +29,11 @@
formControlName="configuration">
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html
index 3fdccba628..4a90cf6ecf 100644
--- a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html
@@ -16,9 +16,9 @@
-->
-
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts
index 05e1448bc3..f20f481824 100644
--- a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts
@@ -89,7 +89,7 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA
let configuration: DeviceTransportConfiguration = null;
if (this.lwm2mDeviceTransportConfigurationFormGroup.valid) {
configuration = this.lwm2mDeviceTransportConfigurationFormGroup.getRawValue().configuration;
- configuration.type = DeviceTransportType.LWM2M;
+ // configuration.type = DeviceTransportType.LWM2M;
}
this.propagateChange(configuration);
}
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html
index e21bb3818a..9e7390e8d7 100644
--- a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html
@@ -16,9 +16,9 @@
-->
-
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html
index f1b5b442ce..0c99fd7040 100644
--- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html
@@ -30,65 +30,9 @@
-
- device.credentials-type
-
-
- {{ credentialTypeNamesMap.get(deviceCredentialsType[credentialsType]) }}
-
-
-
-
- device.access-token
-
-
- {{ 'device.access-token-required' | translate }}
-
-
- {{ 'device.access-token-invalid' | translate }}
-
-
-
- device.rsa-key
-
-
- {{ 'device.rsa-key-required' | translate }}
-
-
-
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
index 73e5ae040f..94083cc10b 100644
--- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
@@ -19,23 +19,9 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
-import {
- FormBuilder,
- FormControl,
- FormGroup,
- FormGroupDirective,
- NgForm,
- ValidationErrors,
- ValidatorFn,
- Validators
-} from '@angular/forms';
+import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { DeviceService } from '@core/http/device.service';
-import {
- credentialTypeNames,
- DeviceCredentialMQTTBasic,
- DeviceCredentials,
- DeviceCredentialsType
-} from '@shared/models/device.models';
+import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
@@ -83,19 +69,10 @@ export class DeviceCredentialsDialogComponent extends
ngOnInit(): void {
this.deviceCredentialsFormGroup = this.fb.group({
- credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
- credentialsId: [''],
- credentialsValue: [''],
- credentialsBasic: this.fb.group({
- clientId: ['', [Validators.pattern(/^[A-Za-z0-9]+$/)]],
- userName: [''],
- password: ['']
- }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])})
+ credential: [null]
});
if (this.isReadOnly) {
this.deviceCredentialsFormGroup.disable({emitEvent: false});
- } else {
- this.registerDisableOnLoadFormControl(this.deviceCredentialsFormGroup.get('credentialsType'));
}
this.loadDeviceCredentials();
}
@@ -110,82 +87,20 @@ export class DeviceCredentialsDialogComponent extends
this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe(
(deviceCredentials) => {
this.deviceCredentials = deviceCredentials;
- let credentialsValue = deviceCredentials.credentialsValue;
- let credentialsBasic = {clientId: null, userName: null, password: null};
- if (deviceCredentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
- credentialsValue = null;
- credentialsBasic = JSON.parse(deviceCredentials.credentialsValue) as DeviceCredentialMQTTBasic;
- }
this.deviceCredentialsFormGroup.patchValue({
- credentialsType: deviceCredentials.credentialsType,
- credentialsId: deviceCredentials.credentialsId,
- credentialsValue,
- credentialsBasic
- });
- this.updateValidators();
+ credential: deviceCredentials
+ }, {emitEvent: false});
}
);
}
- credentialsTypeChanged(): void {
- this.deviceCredentialsFormGroup.patchValue({
- credentialsId: null,
- credentialsValue: null,
- credentialsBasic: {clientId: '', userName: '', password: ''}
- }, {emitEvent: true});
- this.updateValidators();
- }
-
- updateValidators(): void {
- this.hidePassword = true;
- const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
- switch (crendetialsType) {
- case DeviceCredentialsType.ACCESS_TOKEN:
- this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]);
- this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
- this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
- this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
- this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
- break;
- case DeviceCredentialsType.X509_CERTIFICATE:
- this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]);
- this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
- this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
- this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
- this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
- break;
- case DeviceCredentialsType.MQTT_BASIC:
- this.deviceCredentialsFormGroup.get('credentialsBasic').enable();
- this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity();
- this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
- this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
- this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
- this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
- }
- }
-
- private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
- return (group: FormGroup): ValidationErrors | null => {
- if (!controls) {
- controls = Object.keys(group.controls);
- }
- const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
-
- return hasAtLeastOne ? null : {atLeastOne: true};
- };
- }
-
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
- const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
- if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
- deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic);
- }
- delete deviceCredentialsValue.credentialsBasic;
+ const deviceCredentialsValue = this.deviceCredentialsFormGroup.value.credential;
this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue};
this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe(
(deviceCredentials) => {
@@ -193,18 +108,4 @@ export class DeviceCredentialsDialogComponent extends
}
);
}
-
- passwordChanged() {
- const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
- if (value !== '') {
- this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
- if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) {
- this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true});
- }
- this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
- } else {
- this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
- this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
- }
- }
}
diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
index a3d9a73e8a..0aaf8e62e7 100644
--- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
+++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
@@ -29,7 +29,7 @@ import {
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
-import { EntityAction } from '@home/models/entity/entity-component.models';
+import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models';
import { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of } from 'rxjs';
@@ -61,6 +61,8 @@ import {
} from '../../dialogs/add-entities-to-customer-dialog.component';
import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
+import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
+import { BaseData, HasId } from '@shared/models/base-data';
@Injectable()
export class DevicesTableConfigResolver implements Resolve> {
@@ -221,7 +223,7 @@ export class DevicesTableConfigResolver implements Resolve true,
+ isEnabled: () => true,
onAction: ($event, entity) => this.manageCredentials($event, entity)
}
);
@@ -243,7 +245,7 @@ export class DevicesTableConfigResolver implements Resolve true,
+ isEnabled: () => true,
onAction: ($event, entity) => this.manageCredentials($event, entity)
}
);
@@ -253,7 +255,7 @@ export class DevicesTableConfigResolver implements Resolve true,
+ isEnabled: () => true,
onAction: ($event, entity) => this.manageCredentials($event, entity)
}
);
@@ -294,14 +296,14 @@ export class DevicesTableConfigResolver implements Resolve true,
- onAction: ($event) => this.config.table.addEntity($event)
+ onAction: ($event) => this.deviceWizard($event)
},
{
name: this.translate.instant('device.import'),
icon: 'file_upload',
isEnabled: () => true,
onAction: ($event) => this.importDevices($event)
- }
+ },
);
}
if (deviceScope === 'customer') {
@@ -326,6 +328,23 @@ export class DevicesTableConfigResolver implements Resolve>,
+ boolean>(DeviceWizardDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ entitiesTableConfig: this.config.table.entitiesTableConfig
+ }
+ }).afterClosed().subscribe(
+ (res) => {
+ if (res) {
+ this.config.table.updateData();
+ }
+ }
+ );
+ }
+
addDevicesToCustomer($event: Event) {
if ($event) {
$event.stopPropagation();
@@ -480,5 +499,4 @@ export class DevicesTableConfigResolver implements Resolve {
const node: NavTreeNode = data.instance.get_selected(true)[0];
if (this.onNodeSelected) {
- this.onNodeSelected(node, e as Event);
+ this.ngZone.run(() => this.onNodeSelected(node, e as Event));
}
});
this.treeElement.on('model.jstree', (e: any, data) => {
if (this.onNodesInserted) {
- this.onNodesInserted(data.nodes, data.parent);
+ this.ngZone.run(() => this.onNodesInserted(data.nodes, data.parent));
}
});
diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts
index cff9b26c2b..71dcb16a37 100644
--- a/ui-ngx/src/app/shared/models/constants.ts
+++ b/ui-ngx/src/app/shared/models/constants.ts
@@ -60,6 +60,7 @@ export const HelpLinks = {
linksMap: {
outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings',
securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
+ oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/',
ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/',
ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node',
ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node',
@@ -123,7 +124,7 @@ export const HelpLinks = {
widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest',
widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc',
widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm',
- widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static'
+ widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static',
}
};
diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts
index addb726ed5..d1fb40a9d5 100644
--- a/ui-ngx/src/app/shared/models/device.models.ts
+++ b/ui-ngx/src/app/shared/models/device.models.ts
@@ -33,7 +33,7 @@ export enum DeviceProfileType {
export enum DeviceTransportType {
DEFAULT = 'DEFAULT',
MQTT = 'MQTT',
- LWM2M = 'LWM2M'
+ // LWM2M = 'LWM2M'
}
export enum MqttTransportPayloadType {
@@ -68,7 +68,15 @@ export const deviceTransportTypeTranslationMap = new Map(
+ [
+ [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'],
+ [DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'],
+ // [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint']
]
);
@@ -93,16 +101,16 @@ export const deviceTransportTypeConfigurationInfoMap = new Map {
@@ -60,3 +63,99 @@ export interface UpdateMessage {
message: string;
updateAvailable: boolean;
}
+
+export interface OAuth2Settings {
+ enabled: boolean;
+ domainsParams: DomainsParam[];
+}
+
+export interface DomainsParam {
+ clientRegistrations: ClientRegistration[];
+ domainInfos: DomainInfo[];
+}
+
+export interface DomainInfo {
+ name: string;
+ scheme: DomainSchema;
+}
+
+export enum DomainSchema{
+ HTTP = 'HTTP',
+ HTTPS = 'HTTPS',
+ MIXED = 'MIXED'
+}
+
+export const domainSchemaTranslations = new Map(
+ [
+ [DomainSchema.HTTP, 'admin.oauth2.domain-schema-http'],
+ [DomainSchema.HTTPS, 'admin.oauth2.domain-schema-https'],
+ [DomainSchema.MIXED, 'admin.oauth2.domain-schema-mixed']
+ ]
+);
+
+export enum MapperConfigType{
+ BASIC = 'BASIC',
+ CUSTOM = 'CUSTOM'
+}
+
+export enum TenantNameStrategy{
+ DOMAIN = 'DOMAIN',
+ EMAIL = 'EMAIL',
+ CUSTOM = 'CUSTOM'
+}
+
+export interface ClientProviderTemplated extends ClientRegistration{
+ comment: string;
+ createdTime: number;
+ helpLink: string;
+ name: string;
+ providerId: string;
+ tenantId: TenantId;
+}
+
+export interface ClientRegistration {
+ loginButtonLabel: string;
+ loginButtonIcon: string;
+ clientId: string;
+ clientSecret: string;
+ accessTokenUri: string;
+ authorizationUri: string;
+ scope: string[];
+ jwkSetUri?: string;
+ userInfoUri: string;
+ clientAuthenticationMethod: ClientAuthenticationMethod;
+ userNameAttributeName: string;
+ mapperConfig: MapperConfig;
+ id?: EntityId;
+ additionalInfo: string;
+}
+
+export enum ClientAuthenticationMethod {
+ BASIC = 'BASIC',
+ POST = 'POST'
+}
+
+export interface MapperConfig {
+ allowUserCreation: boolean;
+ activateUser: boolean;
+ type: MapperConfigType;
+ basic?: MapperConfigBasic;
+ custom?: MapperConfigCustom;
+}
+
+export interface MapperConfigBasic {
+ emailAttributeKey: string;
+ firstNameAttributeKey?: string;
+ lastNameAttributeKey?: string;
+ tenantNameStrategy: TenantNameStrategy;
+ tenantNamePattern?: string;
+ customerNamePattern?: string;
+ defaultDashboardName?: string;
+ alwaysFullScreen?: boolean;
+}
+
+export interface MapperConfigCustom {
+ url: string;
+ username?: string;
+ password?: string;
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 0fd0e1f7a2..8f89fa8f39 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -54,7 +54,8 @@
"share-via": "Share via {{provider}}",
"continue": "Continue",
"discard-changes": "Discard Changes",
- "download": "Download"
+ "download": "Download",
+ "next-with-label": "Next: {{label}}"
},
"aggregation": {
"aggregation": "Aggregation",
@@ -119,8 +120,74 @@
"general-policy": "General policy",
"max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked",
"minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative",
- "user-lockout-notification-email": "In case user account lockout, send notification to email"
- },
+ "user-lockout-notification-email": "In case user account lockout, send notification to email",
+ "domain-name": "Domain name",
+ "domain-name-unique": "Domain name and protocol need to unique.",
+ "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io",
+ "oauth2": {
+ "access-token-uri": "Access token URI",
+ "access-token-uri-required": "Access token URI is required.",
+ "activate-user": "Activate user",
+ "add-domain": "Add domain",
+ "delete-domain": "Delete domain",
+ "add-provider": "Add provider",
+ "delete-provider": "Delete provider",
+ "allow-user-creation": "Allow user creation",
+ "always-fullscreen": "Always fullscreen",
+ "authorization-uri": "Authorization URI",
+ "authorization-uri-required": "Authorization URI is required.",
+ "client-authentication-method": "Client authentication method",
+ "client-id": "Client ID",
+ "client-id-required": "Client ID is required.",
+ "client-secret": "Client secret",
+ "client-secret-required": "Client secret is required.",
+ "custom-setting": "Custom settings",
+ "customer-name-pattern": "Customer name pattern",
+ "default-dashboard-name": "Default dashboard name",
+ "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.",
+ "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?",
+ "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.",
+ "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?",
+ "email-attribute-key": "Email attribute key",
+ "email-attribute-key-required": "Email attribute key is required.",
+ "first-name-attribute-key": "First name attribute key",
+ "general": "General",
+ "jwk-set-uri": "JSON Web Key URI",
+ "last-name-attribute-key": "Last name attribute key",
+ "login-button-icon": "Login button icon",
+ "login-button-label": "Provider label",
+ "login-button-label-1": "Login with $(Provider label)",
+ "login-button-label-required": "Label is required.",
+ "login-provider": "Login provider",
+ "mapper": "Mapper",
+ "new-domain": "New domain",
+ "oauth2": "OAuth2",
+ "redirect-uri-template": "Redirect URI template",
+ "copy-redirect-uri": "Copy redirect URI",
+ "registration-id": "Registration ID",
+ "registration-id-required": "Registration ID is required.",
+ "registration-id-unique": "Registration ID need to unique for the system.",
+ "scope": "Scope",
+ "scope-required": "Scope is required.",
+ "tenant-name-pattern": "Tenant name pattern",
+ "tenant-name-pattern-required": "Tenant name pattern is required.",
+ "tenant-name-strategy": "Tenant name strategy",
+ "type": "Mapper type",
+ "uri-pattern-error": "Invalid URI format.",
+ "url": "URL",
+ "url-pattern": "Invalid URL format.",
+ "url-required": "URL is required.",
+ "user-info-uri": "User info URI",
+ "user-info-uri-required": "User info URI is required.",
+ "user-name-attribute-name": "User name attribute key",
+ "user-name-attribute-name-required": "User name attribute key is required",
+ "protocol": "Protocol",
+ "domain-schema-http": "HTTP",
+ "domain-schema-https": "HTTPS",
+ "domain-schema-mixed": "HTTP+HTTPS",
+ "enable": "Enable OAuth2 settings"
+ }
+ },
"alarm": {
"alarm": "Alarm",
"alarms": "Alarms",
@@ -756,7 +823,16 @@
"search": "Search devices",
"selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected",
"device-configuration": "Device configuration",
- "transport-configuration": "Transport 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-credential": "Add credential"
+ }
},
"device-profile": {
"device-profile": "Device profile",
@@ -774,6 +850,8 @@
"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",
@@ -782,8 +860,11 @@
"transport-type": "Transport type",
"transport-type-required": "Transport type is required.",
"transport-type-default": "Default",
+ "transport-type-default-hint": "Refers to already existing transport configuration or basic MQTT, HTTP, CoAP settings",
"transport-type-mqtt": "MQTT",
+ "transport-type-mqtt-hint": "Advanced MQTT topic filters and Device profile settings",
"transport-type-lwm2m": "LWM2M",
+ "transport-type-lwm2m-hint": "LWM2M transport type",
"description": "Description",
"default": "Default",
"profile-configuration": "Profile configuration",
@@ -814,7 +895,8 @@
"not-valid-multi-character": "Invalid use of a multi-level wildcard character",
"single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes .",
"multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/# .",
- "alarm-rules": "Alarm rules ({{count}})",
+ "alarm-rules": "Alarm rules",
+ "alarm-rules-with-count": "Alarm rules ({{count}})",
"no-alarm-rules": "No alarm rules configured",
"add-alarm-rule": "Add alarm rule",
"edit-alarm-rule": "Edit alarm rule",
@@ -2171,8 +2253,11 @@
"entity-coordinate-required": "Both fields, latitude and longitude, are required",
"entity-timeseries-required": "Entity timeseries is required",
"get-location": "Get current location",
+ "invalid-date": "Invalid Date",
"latitude": "Latitude",
"longitude": "Longitude",
+ "min-value-error": "Min value is {{value}}",
+ "max-value-error": "Max value is {{value}}",
"not-allowed-entity": "Selected entity cannot have shared attributes",
"no-attribute-selected": "No attribute is selected",
"no-datakey-selected": "No datakey is selected",